20200216のiOSに関する記事は10件です。

Human Interface GuidelineにおけるModalのUIBarButtonItemの規則

iOS13になってから標準でmodalがiOSアプリ内でも使えるようになり、色々なアプリでmodalをみるようになったと思います。今回は凄いニッチな話ですが、UINavigationControllerで表示したmodalのUIBarButtonItemの命名規則についてまとめていきます。

想定する読者

  • アプリ開発をしているUIデザイナー
  • iOSエンジニア
  • Human Interface Guidelinesを勉強したい人

この記事のゴール

この記事を読んだ人・書いた僕自身が今後理由なくしてmodalのUIBarButtonItemの実装でHIGに従ってないアプリを作成しないこと

そもそもmodalとは

Human Interface Guidelinesによると

Modality is a design technique that presents content in a temporary mode that’s separate from the user's previous current context and requires an explicit action to exit. Presenting content modally can:

Help people focus on a self-contained task or set of closely related options
Ensure that people receive and, if necessary, act on critical information

日本語訳すると

modalとはユーザーにとって、前のコンテキストとは別の一時的な状態でコンテンツを提示するデザイン手法であり、終了するには明示的なアクションが必要なものである。モーダルは

  • 自己完結型のタスクまたは強い関連のある一連のタスクにユーザーが集中できるようにする
  • 人々が重要な情報を受け取り、必要に応じて行動する

ものである。

つまり、簡潔に言うとモーダルは

  • タスクを行うためのもの
  • タスクを完了するか、放棄することで閉じるもの

であることがわかります。

サンプル画像

いくつかAppleが出しているiOSアプリのnavigationBarのスクショを掲載します。
ファイル名
ファイル名
ファイル名
ファイル名
ファイル名
ファイル名

サンプル画像を見てわかること

キャンセルボタンと完了を表すボタンのみ

どの画面にもキャンセルまたは完了(を意味するボタン)がついています。

完了と同じ意味なら「完了」でなくていい

完了の他にも「追加」「投稿」など完了の意味を内包していれば完了を使わなくても良さそうなのが読み取れます。

アイコンは使っていない

僕は✖️のアイコンとかも使用すると勝手に思ってたんですが、Appleのアプリには存在しなさそうな雰囲気を感じます。
また、完了に似た「閉じる」のような文言も使ってないようです。

キャンセルはfontWeightがregular

タスクを完了することが目的なのでタスクを完了せず離脱する場合はregularみたいです。

完了を表すボタンはfontWeightがbold

逆に完了する場合はboldを使うのが推奨されるみたいです。

コードベースで見てみる

Documentでもこれまでの話と同じことがより簡潔に述べれられています。(この部分はmodalだけの話ではないです)
)スクリーンショット 2020-02-16 23.47.16.png

なのでこれを実行すると下の画像のようにdone(完了とか)では勝手にboldになるし、plain(キャンセルとか)ではregularになります。
スクリーンショット 2020-02-16 23.49.35.png

自分の作成してるiOSアプリや普段利用しているアプリがどうなってるのか見てみると面白いかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[tips]Checkra1n脱獄時にフリーズしたときの対処法

脱獄してみた

ios13の脱獄ツールがリリースされたそうなので、脱獄してみた。
詳しい方から便利Tweaksをたくさん教えていただいて、非常に助かりました。
が、調子に乗っていろいろインストールしまくってたらrespring後にロック画面でフリーズした。

症状

Tweakインストール後のrespringでフリーズ。ロック画面をカスタムするTweakだったのでおそらく対応してないiosのバージョンをインストールしてしまった。selioから該当するものをアンインストールしたいが、ロック画面でフリーズしてしまうのでそこまでたどり着けない。入獄するとcheckra1nがないのでsileoをいじれない。

対処法

どうしようかと調べたところ、checkra1nにはセーフモードで起動できるオプションがあるらしい。

checkra1nを起動→Options→Safe Modeにチェックを入れる

checkra1nがある状態でセーフモード起動できるので、sileoで該当するTweakをアンインストール。

そしてSafe Modeのチェックを外して再脱獄。できた。

まとめ

まだios13のjailbreakは歴史が浅いので対応しているTweakも少ないっぽい。
とりあえずインストールしてみるときはセーフモードでやるといいですね。

みなさまよい脱獄lifeを♡

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iPhoneとAIでマスクしている人を判別する

現在コロナウィルスが猛威を奮っております。ITパワーで何かできないものでしょうか。

開発環境

  • XCode iOS Swift
  • Vision Framework
    iOSで顔の検出ができます。
  • CoreML Framework
    iOSで学習モデルを扱います。
  • Turi Create
    MACで学習モデルを作成します。

画像判別のまとめ

機械学習にはモデルが必要です。Inception v3という公開モデルには1000個のカテゴリーが入っています。マウスであったり、コンピューターキーボードであったり、PCモニターの前に座った人をバーバーと判別したりします。
結果は、全てのカテゴリーを足して100になるように出ます。ABCDEの5つある場合、A80、B10、C10、D0、E0、のように「強いて言えばどれか」という結果になります。
なので、特定のモノを判別させたければ、違うモノを判別する必要があります。

顔検出と顔判別は以前の記事にあります。
iPhoneで顔認識 - Qiita

学習モデルの作成

今回はマスクするしないの2つだけです。結果はMask80%、None20%のようになります。MaskフォルダとNoneフォルダに顔写真をいっぱい入れて、pythonを実行して学習モデルを作成します。

model.py
import turicreate as tc
import os

data = tc.image_analysis.load_images('train', with_path=True, recursive=True)
data['label'] = data['path'].apply(lambda path: os.path.basename(os.path.dirname(path)))
data.save('Chicken.sframe')

train_data, test_data = data.random_split(0.9)
model = tc.image_classifier.create(train_data, target='label', max_iterations=100)

predictions = model.predict(test_data)
metrics = model.evaluate(test_data)
print('metrics=' + str(metrics['accuracy']))

model.save('Chicken.model')
model.export_coreml('Chicken.mlmodel')

実験

  1. マスクは顔検出できず(ちっきしょー)
    元々のアプリは顔検出 → 画像切り抜き → 顔の判別です。使えなくて残念です。

  2. 全画面を判別するよう修正しました。
    日本人限定、女性限定、正面限定、白マスク限定(試行錯誤しました)
    一応下のサンプルはフリー素材使っています。

  3. 結果
    マスク判定が99なのがおかしいです。不明なものはすべてマスクになっている模様。作業の途中で悟りました。もっと他にいい方法があるはず。

ソース

動かすにはmodelを作成する必要があります。
https://github.com/koji4104/VisionCoreML

まとめ

iPhoneで画像の機械学習を行うアプリは、いろいろ作り直して今回で3つ目です。判別しやすい画像や、トレーニング画像の集め方など、少しづつコツが掴めたと思います。
今回あまりうまくいきませんでしたけど、仮にマスクの判別がうまくいったとして、それからどうするかも課題です。アプリ側で一般的なCMSと連携できる機能があればいいかなと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift5】UnitテストでArray(配列)のMatchableを作った時の備忘録

はじめに

Cuckooを使ったUnitテストで、
Array(配列)のMatchableを作成した際の備忘録を残しておきます。
なお、誤った記述等ございましたら、コメントをいただけますと幸いです。

実際に使用した用途・目的

Bluetoothクラスの実装で、
discoverCharacteristics(_:for:)メソッド
のテストコードを書く際に、このメソッドの引数である[CBUUID]?をMatchableに適合する必要がありました。

CBUUIDは、Bluetoothで使用する特殊な型です。
CBUUIDの使い所については【Swift5】Bluetoothクラス実装の備忘録にも記載しておりますので、宜しければご覧ください。

エラー内容

Argument type '[CBUUID]' does not conform to expected type 'OptionalMatchable'

実際にテストした内容

discoverCharacteristics(_:for:)メソッド
を呼んだ際に、引数のCBUUIDに適切な値がセットされて呼ばれているのかをテストします。

_ = XCTContext.runActivity(named: "Peripheral.discoverCharacteristics is called.", block: { _ in
    let uuidArray: [CBUUID] = [CBUUID(string: Const.Bluetooth.kUUID01),
                               CBUUID(string: Const.Bluetooth.kUUID02),
                               CBUUID(string: Const.Bluetooth.kUUID03)]
    verify(self.peripheral, times(1)).discoverCharacteristics(uuidArray, for: any())
})

解決方法・コード

エラーの解消には以下の2つのextensionが必要でした。

MatchableExtensions.swift
extension Array: OptionalMatchable where Element == CBUUID {
    public var optionalMatcher: ParameterMatcher<[CBUUID]?> {
        return equal(to: self)
    }
}

extension CBUUID: OptionalMatchable {
    public var optionalMatcher: ParameterMatcher<CBUUID?> {
        return equal(to: self)
    }
}

今回は、CBUUID?の配列に適合する必要があったので、Arrayのextensionと、CBUUIDのextensionの2つが必要でした。

まとめ

Cuckooを使っているからといって特殊な対応が必要というわけではなく、
Arrayのextensionが作れれば問題なし!といった感じでした。

応用も、基礎知識を持っていれば対応できるのですね・・・!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS SimulatorでFlutter WebViewのlocalhost参照でページが真っ白になる件

Flutter WebViewでlocalhostのBasic認証ページを参照した際、ページが真っ白の状態で何も表示されない現象に遭遇した。

iOS Simulatorは13.2を使用。

[✓] Flutter (Channel stable, v1.9.1+hotfix.6, on Mac OS X 10.14.6 18G87, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 11.2.1)
[✓] Android Studio (version 3.5)
[✓] VS Code (version 1.42.0)
[✓] Connected device (2 available)

結論から言うと、接続先をlocalhostとしていたところを、ホストマシンのローカルループバックアドレス127.0.0.1にすることで解消された。

iOS SimulatorのSafari上で直接アクセスした際は問題なくページ参照できたが、FlutterアプリのWebView経由ではAndroid Emulatorと同じくIPアドレスに置き換える必要があるということなのだろうか。
Android Emulatorでnet::ERR_CONNECTION_REFUSEDが出た時の対処法 - Qiita

StackOverflow等でそれらしい情報は得られたが、詳細仕様についてのドキュメントが見つからず…いまいちモヤる。

If you have a server running on the machine where you iOS simulator is running, then you need to choose 'http://127.0.0.1' as the URL.
swift - ios simulator access localhost server - Stack Overflow

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITextViewにPlaceHolderを折り返し有りで追加してみる

概要

UITextViewにプレースホルダーを追加してみました。
かつ、プレースホルダーには折り返しをしてくれるようにしてみました。

はじめに

UITextViewにプレースホルダーを追加するだけのやり方であれば、
パイセン等が分かりやすくて素晴らしい記事を公開されてるので、そちらを参考にしてみてください。

UITextViewにプレースホルダーを設定できるようにする(Swift4)
[Swift 4.2] UITextViewにプレースホルダーを追加する[iOS 12]

私自身も大変参考にさせて頂きました?‍♂️
ありがとうございます?‍♂️?‍♂️?‍♂️
というか、ほぼほぼベースはパクらせてもらっ

こんなの

まずはじめに、単純にプレースホルダーをUILabelで追加しただけであれば、
こんな感じに折り返しが効かずに見切れてしまっています。

※『UITextViewのPlaceHolderをここに表示してます。』という文言を設定
Simulator Screen Shot - iPhone 8 - 2020-02-16 at 20.23.00.png

それがこんな感じに!
Simulator Screen Shot - iPhone 8 - 2020-02-16 at 20.22.42.png

もちろんxib上でも折り返して表示してくれます
スクリーンショット 2020-02-16 19.42.06.png

ソースコード

プレースホルダーの表示用として、UILabelをaddSubViewしており、
その際に、superViewであるUITextViewに対してAutolayoutの設定を入れることにより、折り返して表示してくれるようになってます。

PlaceHolderTextView.swift
import UIKit

@IBDesignable
class PlaceHolderTextView: UITextView {

    private let placeHolderLabelOriginPosition = CGPoint(x: 5.0, y: 8.0)
    lazy private var placeHolderLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.font = font
        label.backgroundColor = .clear
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)

        let labelWidth = frame.width - placeHolderLabelOriginPosition.x * 2
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: topAnchor, constant: placeHolderLabelOriginPosition.y),
            label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: placeHolderLabelOriginPosition.x),
            label.widthAnchor.constraint(equalToConstant: labelWidth)
        ])
        return label
    }()

    @IBInspectable var placeHolder: String = "" {
        didSet { reloadView() }
    }
    @IBInspectable var placeHolderColor: UIColor = .lightGray {
        didSet { reloadView() }
    }

    override var text: String! {
        didSet { changeVisiblePlaceHolder() }
    }


    override func awakeFromNib() {
        super.awakeFromNib()
        changeVisiblePlaceHolder()
        NotificationCenter.default.addObserver(self, selector: #selector(textChanged), name: UITextView.textDidChangeNotification, object: nil)
    }

}

extension PlaceHolderTextView {

    private func reloadView() {
        placeHolderLabel.text = placeHolder
        placeHolderLabel.textColor = placeHolderColor
        changeVisiblePlaceHolder()
    }

    private func changeVisiblePlaceHolder() {
        placeHolderLabel.isHidden = placeHolder.isEmpty || !text.isEmpty ? true : false
    }

    @objc private func textChanged(notification: NSNotification?) {
        changeVisiblePlaceHolder()
    }

}

おわりに

そもそも、プレースホルダーに折り返しが必要になるほどの長文を書くシチュエーションに遭遇するのでしょうか・・・?

追記

@am10 さんからのご指摘&ご提案から、コードベースでテキスト代入時にもプレースホルダーの表示切替が可能になりました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(at)PublishedがどのようにSwiftUIのViewを更新するのか

(at)PublishedがどのようにSwiftUIのViewを更新するのか

@Published属性がどのようにSwiftUIのViewを更新するのかについて書いておきます。

WWDC19の動画を見るとそもそも@Published属性がまだ存在していないのと、その当時はObservableObject型がBindableObject型であったり、@ObservedObject属性が@ObjectBinding属性だったりするのでややこしいためです。

私の結論としては@Publishedあると便利なんだけど、コードを書き換えてるときになぜこのプロパティに@Publishedにしたんだろう、という感覚になりやすい。おとなしく手動でやっといたら意図も読みやすくて混乱しなかったのにという気持ちになりやすいからですかね。

@ObservedObject属性として指定されたObservableObjectはSwiftUIにsubscribeされている

  • Viewは@ObservedObject指定したObservableObjectobjectWillChangeをsubscribeしている
    • subscribeし発火されるとViewを更新するようにしている
    • ObservableObjectobjectWillChangeを使い発火させればいい
  • @PublishedはプロパティのwillSetでobjectWillChangeを使い発火させるのを暗黙的に行っている

objectwillchange_subscribe_from_swiftui.png

objectWillChangeとは何か

objectWillChangeはCombineで定義されているObservableObjectを見るとわかります。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ObservableObject : AnyObject {

    /// The type of publisher that emits before the object has changed.
    associatedtype ObjectWillChangePublisher : Publisher  // 制約の追加 Swift4.1(SE-0157) 
       = ObservableObjectPublisher  // デフォルトで利用する型
         where Self.ObjectWillChangePublisher.Failure == Never // 条件

    /// A publisher that emits before the object has changed.
    var objectWillChange: Self.ObjectWillChangePublisher { get }
}
  • プロパティobjectWillChangeSelf.ObjectWillChangePublisherを返す
  • associatedtypeのObjectWillChangePublisher
    • Publisherプロトコルを制約として追加している
    • 言い換えるとObservableObjectはパブリッシャーを保持している
    • デフォルトではObservableObjectPublisherクラスを使う
      • さらにエラーは流さない
      • おそらくカスタマイズしたPublisherも使える

ObservableObjectPublisherとは何か

Combineで用意されたObservableObjectPublisherクラスを確認してみます。

/// The default publisher of an `ObservableObject`.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
final public class ObservableObjectPublisher : Publisher {

    /// The kind of values published by this publisher.
    public typealias Output = Void

    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    public typealias Failure = Never

    public init()

    /// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
    ///
    /// - SeeAlso: `subscribe(_:)`
    /// - Parameters:
    ///     - subscriber: The subscriber to attach to this `Publisher`.
    ///                   once attached it can begin to receive values.
    final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == ObservableObjectPublisher.Failure, S.Input == ObservableObjectPublisher.Output

    final public func send()
}
  • OutputはVoidなのでイベントは (Void) -> Voidのクロージャ
    • .sink { (Void) -> Void in ... }
      • SwiftUIのViewとしては更新されたらbodyが実行される
        • 使用されるオブジェクトのプロパティは全て読みだされるので
          • イベント時のOutputがVoidでも特に気にしない
  • どのような場合にカスタマイズする?
    • Outputを変更したい場合がある?
      • 思いつかないな...

必ずしも@Publishedを使う必要はない

@Publishedを使いたいのは次の優先度のとき

  • ViewがBindingなオブジェクトを取り出してバインディングしたい(読み込みと書き込み)
  • ObservableObjectの中でfilterflatMapしたい
  • objectWillChange.send()を自動でやりたい

RxSwiftのようなUIからストリームを作れる仕組みがないために、Bindingでの値のやり取りが楽なので@Publishedを使いたい。本当はそれ一択かもしれない。

SwiftUIガイドブック

BOOTHで電子書籍として販売しています。

https://booth.pm/ja/items/1829015

読者のターゲットは次のような人を想定しています

  • SwiftUIのレンダリングシステムが自動で更新されるタイミングを知らない
    • 更新のきっかけになる$ObservedObject@Publishedにもやもやしている
    • SwiftUIのViewが自動で更新されるタイミングやその範囲にもやもやしている
  • @State@Binding@ObservedObjectの違いがよくわからない
    • @Environment@EnvironmentObjectの違いもまだ知らない
  • $オペレータを雰囲気で使って雰囲気で修正している
    • コンパイルエラーになるたびに修正している
    • propertyWrapperとdynamicMemberLookupを理解して使っていない

SwiftUIでpropertyWrapperとdynamicMemberLookupがあやふやだなーという気持ちになったままコードを書くことも多いのではないかと思ってます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift5】カラー指定する方法のチートシート永久保存版

この記事を書いた理由

Swiftでのカラー指定の方法は、たくさんあります。
毎回毎回、やりたいカラー指定のやり方を調べるのがめんどくさかったため、自分でカラー指定のチートシートを作成しました。

環境

ツール version
Swift version 5.1.3
Xcode Version 11.3 (11C29)

チートシート

目次

  1. storyboardでカラー指定する方法
  2. UIColorのプロパティでカラー指定する方法
  3. UIColorでRGB(CGFloat型)でカラー指定する方法
  4. UIColorを無理やりRGBでカラー指定する方法
  5. UIColorを16進数のカラーコードでカラー指定する方法
  6. UIColorをRGBでカラー指定する方法
  7. カラーリテラルを使ってカラー指定する方法
  8. Assets.xcassetsで色を管理し、カラー指定する方法 スクリーンショット 2020-02-16 13.30.02.png

他のチートシート

画面遷移のチートシート

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUI VStackこういうことだったのか劇場

背景

SwiftUIにて画面作成するときにほぼ間違いなくお世話になる、VStack、HStackについて、その公式ドキュメントのリファレンスと実装に差異があり戸惑いました。そのときの疑問とそれをどのように整理して解消したのかを記録したものです。

対象とする読者

Swift初心者
SwiftUI初心者

環境

私がこの記事を書いている際に利用しているのは次の環境です。
Xcode 11.3.1
Swift 5.1

はじめに

VStack、HStackは、SwiftUIで画面レイアウトを作るときにほぼ間違いなくお世話になるもので、ビュー(ここでいうビューは、ボタンやテキスト等のUIコントロール部品や、それらをグループ化した子ビューなど広義でのUIコントロール全般を指すものとご理解ください)をレイアウトとして配置するためのコンテナーとして利用され、それ自体もビューとなります。

ビューを縦(垂直)方向に並べるときに利用するのが、VStack
ビューを横(水平)方向に並べるときに利用するのが、HStack
ビューを奥行き方向に並べるときに利用するのが、ZStack

入れ子でこれらを使い分けることで、様々なレイアウトにカスタマイズして画面を構成していきます。

今回は、このうちのVStackを取り上げます。

VStack

イニシャライザの呼び出し方

イニシャライザとは、インスタンスの初期化用メソッドのこと。

公式ドキュメントの記載は次のとおりです。

公式ドキュメント(VStack)
https://developer.apple.com/documentation/swiftui/vstack

// Creating a Stack
init (alignment: HorizontalAlignment, spacing: CGFloat?, @ViewBuilder content: () -> Content)

VStackイニシャライザのパラメータは3つあります。

  • alignment 水平方向の配置位置をHorizontalAlignment構造体のタイププロパティで指定。 .leadeing(左寄せ), .center(中央寄せ), .trailing(右寄せ)等の指定が可能。デフォルトは.center
  • spacing 子ビュー同士の間隔を調整するための余白を指定します。デフォルトはnilでシステムデフォルトの余白が付与されます。
  • content このスタックに並べるビュー(子ビュー、ボタン、テキストなど)をまとめたものを返すクロージャを指定します。

デフォルトのあるパラメータは省略可能です。一番最後のcontentだけは必ず指定が必要です。

実際の使い方

VStack(alignment: .leading, spacing: 10) {
    Text("Hello World!")
    Text("Here We Go!")
}

実行結果
vstack_result1.png

もし私のようなSwift初心者は、ここで少し混乱します。

  • 疑問1 イニシャライザの定義だと引数が3つあるけど、そのうちの最後のcontentが欠落している?
  • 疑問2 その代わりに、{}内にVStack内に表示させたいビュー(Text2つ)を記述している?
  • 疑問3 そもそも{}内にビュー(Text2つ)を改行して列挙してだけのようだけど、こういう書き方していいの?

では、これらの疑問を見ていきます。

疑問1と疑問2について

swiftでは「トレーリングクロージャー式」という記述をすることが可能です。
トレーリングは後方を指し、クロージャー式は{}で囲んだ式のこと。

トレーリングクロージャーというのは、以下のようなSwift独自の構文です。

関数の最後の引数としてクロージャー式を渡す必要があり、クロージャー式が長い場合に、それを関数の引数として記載せずに、関数呼び出しの括弧の後(末尾)にクロージャー式{ }として記述できる

つまり、さきほどの実装例はトレーリングクロージャー式で記述されていたということです。それを利用しなかった場合は次のよう書き換えることができます。

// トレーリングクロージャー式を使わないで書くと・・・
VStack(alignment: .leading, spacing: 10, 
    content: {
      Text("Hello World!")
      Text("Here We Go!")
    }
)

この例では、たしかにイニシャライザの宣言になんとなく寄ってきました。クロージャーの中身が数行なので、読みにくいということもありません。

ですが、クロージャーに記述する行数が増えれば増えるほど、読みずらくなるのは容易に想像できますので、基本はトレーリングクロージャー式を利用して、重たい記述は引数とは別に記述すべきなのですね。とても合理的な考え方だと思います。

疑問3について

疑問1,疑問2 についてはコードの書き方として便利な式が用意されていることを知りました。しかし、疑問3の「そもそも{}内にビュー(Text2つ)を改行して列挙してだけのようだけど、こういう書き方していいの?」については、まだ解決していません。

ここで、改めてVStackのイニシャライザを見てみると、content引数には、@ViewBuilder属性が付与されていることがわかります。

この属性はSwift5.1の新機能である「ファンクションビルダー」が利用されています。
この属性がついたクロージャー内の各行は、暗黙的に1行が1引数となって、ViewBuilderbuildBlockメソッドに渡されます(暗黙的に内部で変換されます)。

つまり、クロージャー内にTextを2行記述しましたが、実際は内部では以下のように2つの引数を受け取るbuildBlockメソッドに渡され、View型のインスタンスを返却してもらっているのです。

// ViewBuilderへの変換がされた後の実装はこんな感じ
//(こちらの実装でも、もちろん表示されます)
VStack(alignment: .leading, spacing: 10, 
    content: ViewBuilder.buildBlock(
              Text("Hello World!"),
              Text("Here We Go!") 
)

ここまで記述すると、ようやくイニシャライザの定義と完全に理論的な一致が確認でき、安心できます。

なお、ViewBuilderでは、VStack、HStackなどクロージャー内部に最大10行のViewまでを引数として置き換えてくれるbuildBlockメソッドが事前に用意されています。VStackに10行分のTextを記述しても正常に画面表示できますが、11行以上記述するとビルドエラーが発生します。時間がある方はぜひ実際に試してみてください。

さいごに

新しい関数(イニシャライザ)を見て、リファレンス(公式ドキュメント)を参照するのは開発者として基本的な行動です。ですが、実際のコードとリファレンスに乖離があると、混乱したり正しい理解の妨げになります。

私も今回のこの記事を自分で整理してアウトプットすることで、頭の中を整理できました。私のようなSwift初心者の方に、この記事が少しでも役立てば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutterアプリのライフサイクル

はじめに

Flutterアプリのライフサイクルについてまとめています。Flutterのライフサイクルというと、

  • アプリ (AppLifecycleState) ← 今回の内容
  • 画面 (StatefulWidget)

の2種類がありますが、今回は上のアプリ自体のライフサイクルについての内容です。

StatefulWidgetのライフサイクルについては、Flutter StatefulWidgetのライフサイクルにまとめていますので、参照してください。

アプリのライフサイクル一覧とプラットフォームの対応関係

AppLifecycleStateの状態遷移と状態一覧を以下に示します。
スクリーンショット 2020-02-17 20.02.01.png

基本的にプラットフォーム側の状態をFlutterアプリの状態として再定義している感じですが、iOSのライフサイクルに近いと思います。なお、iOSのライフサイクルは詳しくなく、AndroidエンジニアのためのiOSのUIViewControllerのライフサイクルとAndroidのActivityのライフサイクル比較を参考にさせていただきました。間違いがあればコメントでご指摘いただけると幸いです。

inactivepausedの違いは、画面が表示されているか否かの違いの様子です。

状態 内容 Android iOS
inactive アプリは表示されているが、フォーカスがあたっていない状態 onStart, onPause viewDidLoad
paused アプリがバックグラウンドに遷移し(最前面に表示されてない)、入力不可な一時停止状態 onPause viewWillDisappear viewDidDisappear
resumed アプリがフォアグランドに遷移し(paused状態から復帰)、復帰処理用の状態 onResume viewWillAppear viewDidAppear
detached アプリが終了する時に通る終了処理用の状態 onDestroy viewWillDisappear

AndroidのonSaveInstanceStateの様な仕組みはあるのか?

現状なさそうです。
AppLifecycleStateの状態を見て、必要であれば自分で対応する必要があります。

状態遷移ユースケース

いくつかのユースケースで状態遷移の動作を確認してみました。

WidgetsBindingObserverを利用するとAppLifecycleStateの状態が取得出来ます。
その他、SystemChannels.lifecycle (詳細はこちらにまとめてます。) を利用しても取得可能です。

確認のためのソースコード

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {

  AppLifecycleState _state;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    print('state = $state');
  }

アプリ起動

イベント発生なし

画面回転

イベント発生なし

アプリ終了

I/flutter (13730): state = AppLifecycleState.inactive
I/flutter (13730): state = AppLifecycleState.paused
I/flutter (13730): state = AppLifecycleState.detached

ホームボタンを押す

I/flutter (13730): state = AppLifecycleState.inactive
I/flutter (13730): state = AppLifecycleState.paused

画面OFF

I/flutter (15366): state = AppLifecycleState.inactive
I/flutter (15366): state = AppLifecycleState.paused

アプリを履歴から復帰

I/flutter (13730): state = AppLifecycleState.resumed

画面分割で起動

I/flutter (13252): state = AppLifecycleState.inactive

画面分割状態でサブ画面 (下画面) のアプリを終了

I/flutter (13252): state = AppLifecycleState.inactive

画面分割状態でサブ画面 (下画面) で新しいアプリを洗濯して、画面を2分割に戻る

I/flutter (13252): state = AppLifecycleState.resumed

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む