はじめに
この投稿はegeniq社の技術ブログを私なりに解釈したものになります。
もし誤っている箇所がありましたらコメントでご指摘ください。
SwiftUIで遭遇するView間のデータ受け渡し方法の疑問
SwiftUIではUI部品は全てViewを継承した構造体であるため、UIKitのUIViewクラスのようにパブリックプロパティや初期化時の引数で値を渡すことができません。UIKitを経験する人間からすると初期の段階で遭遇する疑問だと思います。
結論から言うと、SwiftUIでは親ビューと子ビュー間で値を渡す方法は、下記の3つが挙げられます。
- Environment
- EnvironmentObjects
- Preferences
Environment
Environmentとは、@1amageekさんのEnvironmentValuesを制するものはSwiftUIを制するでも説明の通り、そのViewが持つ環境変数です。独自の環境変数を定義することができ、それを利用して親ビューから任意の値を渡すことが可能です。
通常の標準のEnvironmentの利用方法
子ビュー
struct EditableNameView: View {
@Environment(\.editMode) var mode
@State var name = ""
var body: some View {
TextField("Name", text: $name)
.disabled(mode?.wrappedValue == .inactive)
}
}
上の例だと、editModeと言う名前の環境変数をEditableNameViewが受け付けられることを表しています。そして、modeが.inactiveの場合内包するTextFieldを無効化するという処理を行っています。
利用する親ビューでは次のように値を渡します。
親ビュー
struct ContentView: View {
@State var mode: EditMode = .inactive
var body: some View {
EditableNameView().environment(\.editMode, mode)
}
}
尚、このeditModeと言う環境変数は公式リファレンスによると型がBindingのため、親ビューから渡した変数の値は子ビューで変更されると親もその変更した値を知ることができます
var editMode: Binding?
The mode indicating whether the user can edit the contents of a view associated with this environment.
独自のEnvironmentの利用方法
標準の環境変数だけでは独自の値を渡すことができませんが、EnvironmentKeyというプロトコルを継承することで独自の環境変数を定義することができます。
独自環境変数
struct IntEnvKey: EnvironmentKey {
static var defaultValue: Int = Int()
}
このままでは、キー名を定義しただけで、実際に利用するためにはEnvironmentValuesをextensionで拡張する必要があります。
EnvironmentValuesの拡張
extension EnvironmentValues {
var intEnvKey: Int {
get { self[IntEnvKey.self] }
set { self[IntEnvKey.self] = newValue}
}
}
先ほどと同様に子ビューで、IntEnvKeyに対応していることを宣言します。
子ビュー
struct CounterView: View {
@Environment(\.intEnvKey) var count
var body: some View {
Text("count: \(count)")
}
}
親ビューでも先ほどと同様に.environmentで値を設定します。
親ビュー
struct ContentView: View {
@State var mode: EditMode = .inactive
var body: some View {
CounterView().environment(\.intEnvKey, 10)
}
}
説明の都合上Intを指定しましたが、独自のクラスを指定することもできるため子ビューへの値渡し方法として利用ができます。
EnvironmentObjects
これは公式のチュートリアルでも紹介されている方法で、他の2つに比べて一般的な方法だと思います。利用するためには下記のように独自のクラスを定義する必要があります。
一見すると、値渡しをするためだけにクラスを定義するの?と思ってしまいますが、MVVMのModelとしてクラスを定義してしまえば存在する意義は強くなりますし全体の見通しも良くなると思います。
独自クラス
class Person: ObservableObject {
@Published var firstName: String = ""
@Published var lastName: String = ""
var name : String {
get {
firstName + " " + lastName
}
}
}
子ビュー
struct PersonView: View {
@EnvironmentObject private var person: Person
var body: some View {
VStack {
Text(person.name)
}
}
}
親ビュー
struct ContentView: View {
@ObservedObject var person = Person()
var body: some View {
PersonView().environmentObject(person)
}
}
Preferences
先に紹介した2つの方法は基本的に親から子へ伝達させる方法でしたが、Preferenceは子から親へ伝達させる方法です。
まずはPreferenceKeyを継承して独自のPreferenceを定義します。
独自Preference
struct BoolPreference: PreferenceKey {
typealias Value = Bool
static var defaultValue: Value = false
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue() || value
}
}
子ビューでは自身が生成されたタイミングで独自Preferenceの値をtrueに設定します。
子ビュー
struct StateView: View {
var body: some View {
VStack {
Text("Hello")
}.preference(key: BoolPreference.self, value: true)
}
}
親ビューではonPreferenceChangeを仕掛け、値が変わったタイミングで変わった値を取得するようにします。
親ビュー
struct GameView: View {
@State var state: Bool
var body: some View {
StateView()
.onPreferenceChange(BoolPreference.self) { value in
self.state = value
}
}
}
このようにすることで子から親に値を伝達します。
おわりに
SwiftUIでアプリを組む際には、基本的にはEnvironmentObjectsを使って値を伝達し、必要に応じてEnvironmentとPreferencesを使っていけば良いのかと思います。
それぞれどのような場合に使い分けをするのかについてはまだ知見が固まっておらず、分かったら別途投稿をしたいと思います。