忙しい人向けの Data Essentials in SwiftUI: Part 1 #WWDC20
データ設計
SwiftUI で View を作るときに3つの質問が役立つ。
・必要としているデータは何か?
・どのようにデータを操作するか?
・データはどこから来るのか(Source of Truth)
データモデルの設計において、最後の質問がもっとも重要。
データを操作しない View は状態を let プロパティで宣言でき、Source of Truth は親ビューが BookCard をインスタンス化するときに渡される。
図にするとこんな感じ。
次に「Update Progress」ボタンがタップされた時に、シートを表示する例を考えてみる。
Q. データは何が必要なのか?
A. シートに必要なデータは次の3つになる。
複数の関連する値は Struct でラップすることで、不変性とテスト容易性が保て、値の変更もその単位で扱われるようになる。
こういうときはローカルな状態保持として @State を利用できる。これは SwiftUI において最もシンプルな Source of Truth。
枠線で囲ってあるのは”SwiftUI によって管理される”という意味。View 構造体はレンダリングが終わると破棄されるので、別の場所に保存する必要がある。そして、次にレンダリングされる時に、そこからデータが復元される。
次に ProgressEditor が必要なデータは、EditorConfig 全体。シートはデータを変更する必要があるため var で宣言している。
普通に値として渡すとコピーされるため、ProgressEditor 側で変更しても SwiftUI が管理しているステートは変更されない。
@State を用意しても、新しい Source of Truth を作ってしまう。
共有されたデータにアクセスするのに利用するのは @Binding。書き換え可能な参照を渡し、それを通じて状態の読み書きができる。
$ をつけることで、@State の Projected-value である Binding が取得できるのでそれを渡している。受け取る側は @Binding を利用する。
Binding は SwiftUI の標準コントロールでも多く使われている。$ を使って既存のバインディングから、新しいバインディングを取得できる。
変化しないデータはプロパティ、View が一時的に使用するデータは @State、他の View が所有するデータを変更する場合は @Binding を利用するとよい。
ライフサイクル・データ管理
データの永続化をしたり、副作用を発生させたい場合には ObservableObject を利用できる。
ObservableObject プロトコルは objectWillChange という単一のプロパティを持っていて、それが返す Publisher はオブジェクトを変更する前に通知をする必要がある。
その Publisher はデフォルトですぐに使えるようになっているが、タイマー用の Publisher など自分でカスタムすることもできる。
ObservableObject は Source of Truth となり、値の変更に伴って、依存している View が再レンダリングされる仕組みになる。
値型でデータモデルを定義し、参照型(ObservableObject)でそのライフサイクルを管理することができる。これは1つの ObservableObject で一元管理をする例。
あるいは複数の ObservableObject を用意し、必要なデータだけを公開するようにすることもできる。
現在の進捗を保存するため、ObservableObject に準拠した CurrentlyReading を作成する。本(book)は変更されないので let で宣言し、変更可能な進捗(progress)は @Published で宣言する。
@Published は ObservableObject と強調して動作し、値が willSet で変更されるたびに変更を SwiftUI に通知する。
View から利用するためには @ObservedObject、@StateObject、@EnvironmentObject のいずれかを利用する。
@ObservedObject
@ObservedObject は View の依存関係として管理されるものの、インスタンスは所有しない。
このプロパティの指すインスタンスが Source of Truth となる。
SwiftUI はObservableObject の objectWillChange を購読して、変更されるたびに View が更新される。変更された時(didChange)ではなく、変更される前(willChange)なのは、SwiftUI が更新をひとまとめに行えるようにする為。
Binding は SwiftUI における基本的な原理で、標準のコンポーネントでも使われている。
本を読み終わったことを管理するために、isFinished プロパティを @Published で宣言する。
View 側では Toggle コンポーネントの isOn からバインドしている。先頭に $ をつけることで、値型である isFinished プロパティのバインディングを取得している。
変更は依存しているすべての View に反映される。
@StateObject
リモートから画像を読み込むオブジェクトのように、ある View (ここではカバー画像View)のライフサイクルと紐付けたものが欲しい時がある。
それを実現するのが @StateObject で、ObservableObject は SwiftUI によって所有され、View のライフサイクルと紐付けられて管理される。生成は body プロパティの呼び出しの直前で行われる。
画像の名前を指定して、それを読み込む CoverImageLoader を実装してみる。Image は @Published で宣言されており、読み込みが終わったら SwiftUI に通知される。
View 側のコードはこのようになる。CoverImageLoader は body プロパティの直前に生成され、View の破棄と同時に開放される。
@EnvironmentObject
ObservableObject を View の上位で宣言し、それを引き渡していく必要があるかもしれない。
あるいは宣言位置から離れた場所で利用する場合も。
そのような時に EnvironmentObject が利用できる。
EnvironmentObject は Property wrapper であり、モディファイアでもある。親View のモディファイアで登録して、子View で @EnvironmentObject を利用して参照することができる。
まとめ。
ObservableObject は依存データを宣言し、@ObservedObject はデータへの依存を作り、@StateObject はView のライフサイクルに紐付け、@EnvironmentObject は便利なデータアクセスを提供する。
パート2へ続く
免責
・本記事は公開情報のみに基づいて作成されています。
・要約(意訳)のみなので、詳細はセッション動画をご確認ください。