(2020/11/28 更新)
SwiftUIのデータバインディングの仕組みの一つで、データクラスの更新を監視する@ObservedObjectの使い方を解説します。
これを利用すると複数のViewから同一インスタンスへの同期が可能になります。
その他のデータバインディングについてはこちらの記事を御覧ください。
環境
この記事の情報は次のバージョンで動作確認しています。
【Swift】5.3.1
【iOS】14.2
【macOS】Catalina バージョン 10.15.7
@Stateとの違い
データが更新されるとそのデータを参照しているViewも自動的に更新されるのがSwiftUIの特徴です。
@StateではViewのプロパティに対してその仕組みを実現しますが、データクラスに対しては@ObservedObjectを使用します。
@ObservedObjectを付けて宣言されたデータクラスのインスタンスは、SwiftUIの監視対象となり、プロパティが更新されると参照しているViewが自動的に再描画されます。
基本的な使い方
名前とレベルを保持するUserクラスを例に説明します。
はじめに、対象とするクラスの定義を次のように記述します。
1 2 3 4 5 6 |
class User: ObservableObject { @Published var name = "" @Published var level = 1 } |
次にViewの中で対象のデータクラスを宣言をします。
1 2 3 |
@ObservedObject var user = User() /// インスタンスを生成 |
ポイントは、次の3点です。
- データクラスはObservableObjectプロトコル準拠とする。
- 監視対象とするプロパティに@Published属性を付加する。
- データクラスのインスタンスは@ObservedObject属性を付加してViewの中で宣言する。
これで、@Published属性を付けたプロパティがSwiftUIの監視対象となり、値が変更されると参照している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 |
/// ユーザークラスの定義 class User: ObservableObject { @Published var name = "" @Published var level = 1 } struct ContentView: View { @ObservedObject var user = User() // インスタンスを生成 var body: some View { VStack { Text("\(user.name)さんのレベルは\(user.level)です") /// 入力フォーム TextField("名前", text: $user.name) .textFieldStyle(RoundedBorderTextFieldStyle()) .frame(width: 300) .padding() Button("レベルアップ") { user.level += 1 } } } } |
名前をいれてボタンをタップすると、プロパティを参照しているTextViewが再描画されます。
インスタンスを子Viewと共有する
Swiftのclassインスタンスは参照型ですので、そのまま子Viewに渡すだけで参照・更新が可能となります。
次のコードは入力フォームを別のViewに分割し、そちらからも同じインスタンスと同期をとる例です。
入力フォームView側でも共有するインスタンスを@ObservedObject属性を付けて宣言します。
バケツリレーのように、子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 31 32 33 34 35 |
/// ユーザークラスの定義 class User: ObservableObject { @Published var name = "" @Published var level = 1 } struct ContentView: View { @ObservedObject var user = User() var body: some View { VStack { Text("\(user.name)さんのレベルは\(user.level)です") /// 入力フォーム InputForm(user: user) // 入力フォームViewにUserクラスのインスタンスを渡す } } } /// 入力フォーム struct InputForm: View { @ObservedObject var user: User // Userクラスのインスタンスを共有する var body: some View { VStack { TextField("名前", text: $user.name) .textFieldStyle(RoundedBorderTextFieldStyle()) .frame(width: 300) .padding() Button("レベルアップ") { user.level += 1 } } } } |
インスタンスの一部プロパティだけ子Viewに渡すケース
インスタンスの一部プロパティだけを子Viewと共有したい場合は、@Stateで宣言されたプロパティと同様の方法が可能です。
子Viewに対しては、頭に$を付けてプロパティの参照値を渡し、子View側では@Bindingを付加して対象プロパティを宣言します。
以下はレベルアップボタンのみ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 31 32 33 |
/// ユーザークラスの定義 class User: ObservableObject { @Published var name = "" @Published var level = 1 } struct ContentView: View { @ObservedObject var user = User() var body: some View { VStack { Text("\(user.name)さんのレベルは\(user.level)です") /// 入力フォーム TextField("名前", text: $user.name) .textFieldStyle(RoundedBorderTextFieldStyle()) .frame(width: 300) .padding() LevelUpButton(level: $user.level) // レベルアップボタンView } } } /// レベルアップボタン struct LevelUpButton: View { @Binding var level: Int // 親Viewの値を参照する var body: some View { Button("レベルアップ") { level += 1 } } } |