SwiftUI の @State と @Binding
iOS 登場時からの UIライブラリである UIKit とは異なり、2019年に発表された SwiftUI は宣言的に UI を記述できる。ユーザ操作やネットワークからのデータ受信などの各種イベントに起因してデータが変更されると、UI に自動で反映される。データを UI に反映させるロジックを書かなくても良いその仕組みは、どのように実現されているのだろうか。
View の構造とアプリケーションの状態
SwiftUI の View は View
プロトコルを実装した struct
として表現されている。この View struct は、内部に他の View struct を複数包含することができる。複雑な図形を表現するときによく使われる Composite Pattern になっているといえる。
By Composite_UML_class_diagram.svg: Trashtoy derivative work: Aaron Rotenberg , Public Domain, Link
例えば、以下のようなクラス図で表現される図形モデルを考えてみる。直線(line)と円(circle)、そしてそれらを複数包含可能な CompositeShape
からなる、シンプルな図形モデルである。
このモデルで表現可能な図には、例えば以下のようなオブジェクト図で表現できるものがある。
実際の図形にすると、こうなる。
SwiftUI も、上記の例よりはるかに複雑ではあるが、Viewプロトコル(上記の例における Shape に相当)を実装した struct を階層状に組み合わせることで、iOSアプリが必要とするあらゆる画面を構築できるようになっている。
SwiftUI は、アプリケーション全体を一つの View struct のオブジェクト包含関係で表現している。アプリケーションで使うすべての UI要素が、この包含関係の一部分として階層化されている。刻々と変わるアプリケーションの状態(データ、モデル)に応じて、どの「一部分(View)」を表示するかを if - else
で制御する。ボタンを表示する、しないなどの細かい制御から、ページ遷移などの画面全体を書き換える制御まで、この方式で実装する。
アプリケーションの状態(データ、モデル)は、個々の View の中に埋め込むこともできるし、View とは独立させて持つこともできる。個々の View struct の中に分散させて持つのは、その View が独立性が高いコンポーネントとみなせる場合だ。例えばトグルスイッチは最もシンプルなものの一つだろう。ON であるか OFF であるかは、トグルスイッチコンポーネントの中に持たせた方が良い。一方、アプリケーション内の Viewオブジェクト階層の複数箇所から参照されるようなデータは、View とは独立させて持つ方が良い。MVCモデルにおける「モデル」と呼ばれるデータがこれに相当する。
View に持たせるか、View から独立させるかは、そのデータが View のオブジェクト階層構造のどこで使われるかによる。
@State
SwiftUI では、View に持たせるデータを struct の中のプロパティとして定義する。最初に一回表示するだけなら、これで事足りる。
struct SampleView: View {
var name: String
var body: some View {
Text(name)
}
}
しかし name
プロパティが何らかの原因で変更されたときに表示に自動反映したいと思うなら、これを @State
プロパティラッパとして定義する。これだけで、値が UI に自動反映されるようになる。
struct SampleView: View {
@State var name: String
var body: some View {
Text(name)
}
}
@State と @Binding
name
を別のサブView AnotherView
に渡す場合、表示したいだけなら以下のようにすれば良い。
struct SampleView: View {
@State var name: String
var body: some View {
AnotherView(name: name)
}
}
struct AnotherView: View {
var name: String
var body: some View {
Text(name)
}
}
AnotherView
で name
の値を変更したい場合は、以下のように Binding
として渡す必要がある。
struct SampleView: View {
@State var name: String
var body: some View {
VStack {
Text(name)
AnotherView(name: $name) // $ を付けると Binding になる
}
}
}
struct AnotherView: View {
@Binding var name: String // Binding として定義する
var body: some View {
TextField("name", text: name)
}
}
Binding
にすると、AnotherView
での name
に対する変更はすべて Binding
でプロキシされて SampleView
の name
プロパティに反映され、結果として SampleView
UI の Text(name)
による表示も変更される。
この、ある事実(データ)を一カ所(この例では name
)に集約し、コピーしないことによってデータの整合性を保つ設計概念を Single Source of Truth と呼ぶ。