公式ドキュメントを読んでSwiftUIにおけるViewとデータのハンドリングの知識が深まったので、理解の再確認のためにも情報をまとめました。基本的に公式ドキュメントの要約です。
なお、過去にこちらの記事でView間のデータ受け渡し方法の紹介として、EnvironmentやPreferencesを紹介しましたが、基本はState、Binding、ObservableObject(EnvironmentObject)だと考えています。
指摘がありましたら、是非コメントください。
基本のStateとBinding
State
- Viewの表示内容を制御するための変数
- publicで宣言した場合は親Viewからコンストラクタ引数として値を受け取ることが可能(初期値は不要)
- privateで宣言した場合は初期値が必須
- 自分自身で利用するか子Viewへの値渡しとして利用可能
- 親Viewから引数で受け取ったStateは子Viewで変更したとしてもその変更は親には伝わらない
Binding
- publicで宣言したStateと同じく、親から値を受け取る
- 親Viewから受け取ったStateの値変更を親に伝えたいときに利用する
StateとBindingは近い親子関係や自分自身で完結する場合にのみ使うべきです。
図で書くとこんな感じです。
複数Viewを考慮すると必要になるObservableObject(EnvironmentObject)
先ほどのStateとBindingを使えばView間のデータのやり取りは可能です。
しかし、親子関係を持たないView間(図2)や階層の深いViewへのデータ渡し(図3)はどうすれば良いでしょうか?
そのような場合はStateのバケツリレーではなくObservableObjectを使います。
ObservableObject
class Book: ObservableObject {
@Published var title = "Great Expectations"
}
- イメージとしてはState変数を複数持てる構造体
- 利用側がObservedObjectとして定義した場合、Published宣言された変数の変更を検知することができる
- Stateのようにコンストラクタ引数にObservedObjectを渡すことで子Viewで値の参照&変更をすることができる
- 子View側でPublished宣言された変数を変更すると、親View側で変更を検知することができる
EnvironmentObject
コンストラクタ引数で渡さないと行けないのであれば、先に挙げた課題は解決しないとのではと思った方は鋭いです。
いちいち、バケツリレーするのは面倒ですし、コピーミスが発生する恐れがあります。
そのような場合は EnvironmentObject を使います。親ViewでEnvironmentObjectとして、ObservableObjectインスタンスの指定があれば、そのViewの全ての子孫ViewがObservableObjectの値を参照することができます。
また、図2の画面AとBは直接の親子関係はありませんが、SwiftUIは全ての画面はSceneDelegateで指定されるRootViewの子孫関係にあり、元を辿れば必ず同じViewを見つけることができます。
struct BookReader: App {
@StateObject var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
- 上の例で言うと、LibraryがObservableObject
- @StateObjectはObservableObjectの大元、Stateと同じく値を保持するため@StateObjectとして宣言する
- LibraryView以下の子孫にLibraryを参照変更させたい場合はenvironmentObject()でLibraryのインスタンスを指定する
struct LibraryView: View {
@EnvironmentObject var library: Library
// ...
}
- 利用側はEnvironmentObjectとしてメンバ定義を行う
- 親のenvironmentObject()で指定されたインスタンスがここに入る
- library内の@Published宣言された値を変更すると、LibraryをEnvironmentObjectとして保持している親と子孫にその変更が伝わる
EnvironmentObjectを使った時のイメージ
まとめ
- 親と子のような関係が浅いView間のデータやり取りはState/Bindingで十分
- 親子関係の無いView間でのデータやり取りはObservableObject(EnvironmentObject)を使う
- 親、孫、ひ孫といった深いView間のデータやり取りはObservableObject(EnvironmentObject)を使う