【SwiftUI】@Stateの基本的な使い方
概要
前回の記事では、propertyWrapperの基本について書きました。
SwiftUIではこのpropertyWrapperの機能を使って様々な機能が提供されています。
今回は、値の更新を検知してViewを再描画させるためのpropertyWrapper @State
についてみていきます。
環境
- XCode: 11.4
- Swift: 5.2
@Stateとは
@State は Viewの更新を監視するための propertyWrapper で、SwiftUIを使って画面を実装するための基本的な機能の1つです。
@StateをつけたView内のプロパティが変更されるとViewの body
プロパティが再評価されViewが更新されます。
次のコードは簡単なサンプルです。
struct ContentView: View { @State private var count = 0 var body: some View { VStack { Text("Count: \(count)") Button(action: { self.count += 1 }) { Text("count up") } } } }
上記サンプルでは、count
というViewプロパティが @Stateで宣言されています。
「count up」のラベルがつけられたボタンを押下すると count
プロパティの値が1つずつ加算され、その変更を検知するたびにViewが再描画されます。
@Stateで宣言されたプロパティの特徴
@Stateで宣言されたプロパティの特徴で気になったものを記述します。
① mutating キーワードをつけた関数以外から更新できる
Viewは構造体のため、プロパティを構造体内部から更新するためには、mutating
キーワードをつけたメソッドから更新する必要がありますが、@Stateをつけたプロパティは View内のどこからでも値の更新が可能
です。
② View内部と子View内からしか参照/変更できない
@Stateをつけたプロパティは、基本的にView内部(bodyのgetter内)とその子View内からのみ参照/更新されます。
そのため@Stateのプロパティは private
として宣言するのが推奨のようです。
③ Viewが再描画(再初期化)されても@Stateの値を保持する
SwiftUIのView構造は、親View内から子Viewを宣言する階層構造になっており、基本的に親Viewが再描画されるタイミングでは子Viewも再描画されます。
(再描画時には、init()
が呼ばれ構造体自体が再生成されます。)
その時、子Viewが @Stateのプロパティを持っていた場合、再描画されたタイミングで@Stateの値は初期化されません
。
次のサンプルをみてみます。
struct ParentView: View { @State private var count = 0 var body: some View { VStack { Text("Count: \(count)") Button(action: { self.count += 1 }) { Text("count up") } // NOTE: 子Viewの宣言 ChildView() } } } struct ChildView: View { @State private var text = "" init() { // NOTE: 親ViewのStateが変わるたびにinitが呼び出される。 print("init child view.") } var body: some View { VStack { // NOTE: 構造体が作り直されても @Stateで保持している値は初期化されない Text("Text: \(text)") Button(action: { self.text = "Hello" }) { Text("set text") } } } }
ParentViewは、count
という @Stateのプロパティを持っています。
また、子Viewとして ChildViewをbody内で描画しています。
ChildViewは @Stateのプロパティとして text
という文字列を持っています。
このコードを実行し、まずChildViewの「set text」ボタンを押下します。
このタイミングで、ChildViewの text
プロパティに「”Hello”」という文字列がセットされます。
次に、ParentViewの「count up」ボタンを押下します。
このタイミングでcount
プロパティが更新されParentViewは再描画されます。
同時にChildViewも作り直されます。
この時、先程設定した text
プロパティの文字列「”Hello"」の値は構造体が変わっても初期化されずに残ります。
これは@StateのpropertyWrapperで管理する内部の値が独自のストレージ管理をされているからのようですが、詳細の仕組みまではよくわかっていません。
@Stateで宣言するプロパティのデータ型
@Stateで管理できるプロパティのデータ型について、基本的なパターンを見ていきます。
基本データ型
Int
や String
など基本データ型を @Stateで管理する場合です。
struct ContentView: View { @State var name = "" var body: some View { return VStack { Text("name: \(name)") Button(action: { self.setName() }) { Text("set string name") } } } func setName() { name = "A" } }
これは、特に意識せず値を再代入することでViewが再描画できます。
構造体
@Stateで管理するデータ型が構造体(struct)の場合です。
struct ContentView: View { struct User { var name = "" } @State var user: User = User() var body: some View { return VStack { Text("name: \(user.name)") Button(action: { self.setName() }) { Text("set struct name") } } } func setName() { user.name = "A" } }
@Stateで管理するデータ型が構造体の場合、構造体のプロパティの値が変わってもViewは再描画されます。
func setName() { var u = User() u.name = "A" user = u }
このように構造体自体を作り直す必要はありません。(上記でも再描画は発生します)
クラス
@Stateで管理するデータ型がクラス(class)の場合です。
struct ContentView: View { class User { var name = "" } @State var user: User = User() var body: some View { return VStack { Text("name: \(user.name)") Button(action: { self.setName() }) { Text("set class name") } } } func setName() { user.name = "A" } }
クラスのプロパティを変更しても、Viewの変更は検知されずViewは再描画されません
。
ただし、次のようにインスタンス自体を更新した場合は、再描画されます。
func setName() { var u = User() u.name = "A" user = u }
クラスを監視対象として使用したい場合は、別に @ObservedObject
というpropertyWrapperが提供されているのでそちらを使うのが一般的です。