忙しい人向けの Data Essentials in SwiftUI: Part 1 #WWDC20

データ設計

SwiftUI で View を作るときに3つの質問が役立つ。

・必要としているデータは何か?
・どのようにデータを操作するか?
・データはどこから来るのか(Source of Truth)

データモデルの設計において、最後の質問がもっとも重要。

画像1

データを操作しない View は状態を let プロパティで宣言でき、Source of Truth は親ビューが BookCard をインスタンス化するときに渡される。

画像2

図にするとこんな感じ。

画像3

次に「Update Progress」ボタンがタップされた時に、シートを表示する例を考えてみる。

画像4

Q. データは何が必要なのか?
A. シートに必要なデータは次の3つになる。

画像5

複数の関連する値は Struct でラップすることで、不変性とテスト容易性が保て、値の変更もその単位で扱われるようになる。

画像6

こういうときはローカルな状態保持として @State を利用できる。これは SwiftUI において最もシンプルな Source of Truth。

画像8

枠線で囲ってあるのは”SwiftUI によって管理される”という意味。View 構造体はレンダリングが終わると破棄されるので、別の場所に保存する必要がある。そして、次にレンダリングされる時に、そこからデータが復元される。

画像9

次に ProgressEditor が必要なデータは、EditorConfig 全体。シートはデータを変更する必要があるため var で宣言している。

画像10

普通に値として渡すとコピーされるため、ProgressEditor 側で変更しても SwiftUI が管理しているステートは変更されない。

画像11

@State を用意しても、新しい Source of Truth を作ってしまう。

画像12

共有されたデータにアクセスするのに利用するのは @Binding。書き換え可能な参照を渡し、それを通じて状態の読み書きができる。

画像13

をつけることで、@State の Projected-value である Binding が取得できるのでそれを渡している。受け取る側は @Binding を利用する。

画像14

Binding は SwiftUI の標準コントロールでも多く使われている。$ を使って既存のバインディングから、新しいバインディングを取得できる。

画像15

変化しないデータはプロパティ、View が一時的に使用するデータは @State、他の View が所有するデータを変更する場合は @Binding を利用するとよい。

画像16

ライフサイクル・データ管理

データの永続化をしたり、副作用を発生させたい場合には ObservableObject を利用できる。

画像17

ObservableObject プロトコルは objectWillChange という単一のプロパティを持っていて、それが返す Publisher はオブジェクトを変更するに通知をする必要がある。

画像18

その Publisher はデフォルトですぐに使えるようになっているが、タイマー用の Publisher など自分でカスタムすることもできる。

画像19

ObservableObject は Source of Truth となり、値の変更に伴って、依存している View が再レンダリングされる仕組みになる。

画像20

値型でデータモデルを定義し、参照型(ObservableObject)でそのライフサイクルを管理することができる。これは1つの ObservableObject で一元管理をする例。

画像21

あるいは複数の ObservableObject を用意し、必要なデータだけを公開するようにすることもできる。

画像22

現在の進捗を保存するため、ObservableObject に準拠した CurrentlyReading を作成する。本(book)は変更されないので let で宣言し、変更可能な進捗(progress)は @Published で宣言する。

画像23

@Published は ObservableObject と強調して動作し、値が willSet で変更されるたびに変更を SwiftUI に通知する。

画像24

View から利用するためには @ObservedObject@StateObject@EnvironmentObject のいずれかを利用する。

画像25

@ObservedObject

@ObservedObject は View の依存関係として管理されるものの、インスタンスは所有しない。

画像26

このプロパティの指すインスタンスが Source of Truth となる。

画像27

SwiftUI はObservableObject の objectWillChange を購読して、変更されるたびに View が更新される。変更された時(didChange)ではなく、変更される前(willChange)なのは、SwiftUI が更新をひとまとめに行えるようにする為。

画像28

Binding は SwiftUI における基本的な原理で、標準のコンポーネントでも使われている。

画像29

本を読み終わったことを管理するために、isFinished プロパティを @Published で宣言する。

画像30

View 側では Toggle コンポーネントの isOn からバインドしている。先頭に $ をつけることで、値型である isFinished プロパティのバインディングを取得している。

画像31

変更は依存しているすべての View に反映される。

画像32

@StateObject

リモートから画像を読み込むオブジェクトのように、ある View (ここではカバー画像View)のライフサイクルと紐付けたものが欲しい時がある。

画像33

それを実現するのが @StateObject で、ObservableObject は SwiftUI によって所有され、View のライフサイクルと紐付けられて管理される。生成は body プロパティの呼び出しの直前で行われる。

画像34

画像の名前を指定して、それを読み込む CoverImageLoader を実装してみる。Image は @Published で宣言されており、読み込みが終わったら SwiftUI に通知される。

画像35

View 側のコードはこのようになる。CoverImageLoader は body プロパティの直前に生成され、View の破棄と同時に開放される。

画像36

@EnvironmentObject

ObservableObject を View の上位で宣言し、それを引き渡していく必要があるかもしれない。

画像38

あるいは宣言位置から離れた場所で利用する場合も。

画像37

そのような時に EnvironmentObject が利用できる。

EnvironmentObject は Property wrapper であり、モディファイアでもある。親View のモディファイアで登録して、子View で @EnvironmentObject を利用して参照することができる。

画像39

まとめ。

ObservableObject は依存データを宣言し、@ObservedObject  はデータへの依存を作り、@StateObject はView のライフサイクルに紐付け、@EnvironmentObject は便利なデータアクセスを提供する。

画像40

パート2へ続く

免責

・本記事は公開情報のみに基づいて作成されています。
・要約(意訳)のみなので、詳細はセッション動画をご確認ください。

役に立った記事などありましたらサポート頂けると嬉しいです。