(2021/05/22 更新)
SwiftUIのデータバインディングの仕組みの一つである、@Stateについて解説します。
プロパティが更新された場合に、参照しているViewも同時に更新される仕組みが実現できます。
その他のデータバインディングについてはこちらの記事を御覧ください。
環境
この記事の情報は次のバージョンで動作確認しています。
【Swift】5.4
【iOS】14.5
【macOS】Big Sur バージョン 11.1
@Stateの概要
@Stateはプロパティの宣言時に使えるSwiftUIのカスタム属性です。
プロパティの値とUIの状態を自動的に同期する仕組みを実現します。
@Stateをつけたプロパティには次の2つの機能が付加されます。
- 値が更新可能になる。
SwiftUIのViewはStructの為、通常ではプロパティを更新できませんが、@Stateを付けると更新可能になります。 - 値の変更がSwiftUIよってモニタリングされる。
モニタリングしたプロパティに変更があった場合、対象プロパティを参照しているViewが自動的に再描画されます。これにより、プロパティの値とUIの状態の同期が実現されます。
サンプルソース
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct ContentView: View { @State private var isPowerOn = false // 電源の状態を保持するプロパティ var body: some View { VStack { /// 電源ボタン Button(action: { self.isPowerOn.toggle() // クリックでisPowerOnの値を反転 }) { Image(systemName: "power") // 電源ボタンの画像 } /// 電源状態表示 Text(isPowerOn ? "電源ON" : "電源OFF") } .font(.largeTitle) } } |
isPowerOnプロパティが@Stateで宣言されており、電源ボタンのクリックで値のtrue/falseが切り替わります。
isPowerOnプロパティが更新されると、それに合わせてテキストが自動的に再描画されます。
@State使用時の注意点
@Stateで宣言されたプロパティはそれを保持するViewと、そのプロパティを参照する配下のViewからしかアクセスできません。この為、private修飾子の使用をAppleでは推奨しています。
外からは値を設定できない為、プロパティは初期値が必要になります。
View内のみで使うデータフローを扱うのに適している仕組みです。
イニシャライザで値を設定する場合
@Stateプロパティは、イニシャライザで値を設定する場合、書き方に特徴があります。
次のようにそのまま設定しても、無視されます。
1 2 3 4 5 6 |
/// イニシャライザ init() { isPowerOn = true } |
正しく値を設定したい場合は、次のようにします。
- 対象プロパティ名の頭に"_"を付加します。
- 設定する値を State(initialValue: 値) の表記にます。
1 2 3 4 5 6 |
/// イニシャライザ init() { _isPowerOn = State(initialValue: true) } |
子Viewに値を渡す
先程のサンプルソースの電源ボタンの部分をカスタムView化して分割した例を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
struct ContentView: View { @State private var isPowerOn = false // 電源の状態を保持するプロパティ var body: some View { VStack { /// 電源ボタン PowerButton(isPowerOn: $isPowerOn) // 子Viewの呼び出し /// 電源状態表示 Text(isPowerOn ? "電源ON" : "電源OFF") } .font(.largeTitle) } } // 電源ボタンのカスタムView struct PowerButton: View { @Binding var isPowerOn: Bool // 親Viewの値を参照する var body: some View { /// 電源ボタン Button(action: { isPowerOn.toggle() // クリックでisPowerOnの値を反転 }) { Image(systemName: "power") // 電源ボタンの画像 } } } |
@Stateで宣言されたプロパティを子Viewに渡す時は、プロパティ名の頭に$を付けます。
プロパティの値そのものではなく、プロパティへの参照を渡すイメージです。
子View側では、@Stateではなく参照を示す@Bindingでプロパティを宣言します。
自らは値を保有せずに、親のプロパティへの参照を表します。
これにより、子View内でのプロパティ更新は、親のプロパティに反映(=Viewにも反映)されます。
同様の方法で、子Viewのさらに子Viewへと親プロパティの参照値を受け継げます。
子Viewのイニシャライザで受け取る場合
子View側のイニシャライザで@Bindingプロパティに値を設定する場合、書き方に特徴があります。
1 2 3 4 5 6 |
/// イニシャライザ init(isPowerOn: Binding<Bool>) { self._isPowerOn = isPowerOn // プロパティ名の前に"_"を入れる } |
注意点が2点
- 引数の型は、"Binding<型> "と表します。
- インスタンスプロパティ名の頭に"_"を付加して記述します。
インスタンスプロパティ名に"_"を付けずに、self.isPowerOnとすると、次のようなコンパイルエラーになります。
1 2 3 |
Cannot assign value of type 'Binding<Bool>' to type 'Bool' |
標準部品に値を渡す
ToggleやTextFieldのような標準部品でも、この仕組みを利用しています。
例えば、以下はToggleのイニシャライザです。
1 2 3 |
init<S>(_ title: S, isOn: Binding<Bool>) where S : StringProtocol |
引数isOnの型がBinding<Bool>になっているのがわかります。
先程のサンプルの子View呼び出しの部分を次のコードに置き換えるだけで、電源ボタンの代わりにToggleボタンが使用できます。
1 2 3 4 5 |
/// 電源ボタン Toggle("", isOn: $isPowerOn) .labelsHidden() |
構造体とクラスについて
値型である構造体に対しては、他の一般的な型と同様に@Stateが使用可能です。
しかし参照型であるクラスに対しては、うまく動きません。SwiftUIがクラスの値変更を感知できず、Viewが再描画されない為です。
データクラスの変更を監視してViewと同期をとる仕組みとしては@ObservedObjectが別に用意されています。
参考リンク
イニシャライザで@Stateプロパティの値を設定する方法については、こちらの記事を参考にさせてもらいました。