2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Observableなモデルに適応する時のproperty wrapperの使い分け

Posted at

Observationを使ったモデルをSwiftUIのViewで保持するときに、以下property wrapperをつけることになると思います。

  • @State
  • @Environment
  • @Binding
  • @Bindable

どのような時に何のproperty wrapperを使うのが良いのかについて、自分の理解の整理も兼ねて記事にします。

この記事はAppleの公式ドキュメントをもとに書きましたが、一部私の解釈が含まれている部分もあります。
もし誤りなどありましたら、コメントいただけると嬉しいです。

この使い分けについては、WWDCの「Discover Observation in SwiftUI」で語られていたため、今回はこれを参考にしていきます。

image.png

Observationのモデルにおけるproperty wrapperの使い分けは、上記の図で全て説明されていました。

  1. モデルがViewの一部であれば@State
  2. グローバルにモデルを参照したい場合は@Environment
  3. バインディングが必要なだけの場合は@Bindable
  4. それ以外はproperty wrapperなし

ただ自分はこれじゃすぐには理解できなかったので、具体例を見つつ理解を深めていきます。

@Stateの使い所

When the view needs to have its own state stored in a model, use the @State property.

【日本語訳】
ビューが独自の状態をモデルに保存する必要がある場合は、@State プロパティを使用します。

WWDCの動画では上記のように語られていますが、自分の理解としては、
以下全てに該当しているときに使うものと解釈しています。

  • サブビューではないこと
  • View内でモデルのプロパティをBindingで渡す必要がある
@Stateの使用ケース
struct DonutListView: View {
    var donutList: DonutList
    @State private var donutToAdd: Donut?

    var body: some View {
        List(donutList.donuts) { DonutView(donut: $0) }
        Button("Add Donut") { donutToAdd = Donut() }
            .sheet(item: $donutToAdd) {
                TextField("Name", text: $donutToAdd.name)
                Button("Save") {
                    donutList.donuts.append(donutToAdd)
                    donutToAdd = nil
                }
                Button("Cancel") { donutToAdd = nil }
            }
    }
}

上記はWWDCの動画で紹介されていた、@Stateの使用ケースのコードです。

.sheet(item: $donutToAdd)の部分で、Bindingを渡す必要があります。

そのため、このコードでは、以下全ての条件を満たしていると言えます。

  • 他の画面と共有する必要がなく、そのビューの見た目や動作を制御するためだけに使われる
  • View内でモデルのプロパティをBindingで渡す必要がある

@Environmentの使い所

@Environment. Environment lets values be propagated as globally accessible values.
【日本語訳】
@Environment。Environment を使用すると、値をグローバルにアクセス可能な値として伝播できます。

これは先ほどよりも比較的わかりやすいと思いました。
Observableなモデルの状態をグローバルに参照したい場合は@Environmentを使いましょうということですね。

@Environmentの使用ケース
@Observable class Account {
  var userName: String?
}

struct FoodTruckMenuView : View {
  @Environment(Account.self) var account

  var body: some View {
    if let name = account.userName {
      HStack { Text(name); Button("Log out") { account.logOut() } }
    } else {
      Button("Login") { account.showLogin() }
    }
  }
}
@Environmentの補足コード
@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
        }
        .environment(Account()) // アプリのエントリーポイントで`Account`を注入
    }
}

上記はWWDCの動画で紹介されていた、@Environmentの使用ケースのコードです。
上記コードだけではちょっとグローバルに参照するというイメージが湧きづらいので、追加の補足コードを付けています。

SampleAppというアプリのエントリーポイントでAccountenvironmentに注入しています。
こうすることで、アプリ全体で@Environment(Account.self) varとすることでAccountの状態を共有することができます。

FoodTruckMenuViewでは親ViewからinitAccountのインスタンスを渡してもらうことなくAccountを参照できます。

.environment(Account())は付与したView or Scene以下に環境変数を注入できるため、付与する位置によってスコープを限定することもできます。
以下のようにすれば、FoodTruckMenuView以下でAccountの状態を共有できます。

FoodTruckMenuView()
    .environment(Account())

@Bindableの使い所

The bindable property wrapper is really lightweight. All it does is allow bindings to be created from that type.
【日本語訳】
プロパティラッパーファミリーの最新版は「@Bindable」です。このバインド可能なプロパティラッパーは非常に軽量で、その型からバインディングを作成できるようにするだけです。

WWDCの動画ではBindingを作成できるだけの軽量なproperty wrapperだと語られています。
ただ、自分の試した限り@State@Binableの挙動の違いはほとんどありませんでした。
そこで@Bindableの使い所のヒントとして、@Stateの使い所では、
「ビューが独自の状態をモデルに保存する必要がある場合」とあり、これをサブビューではないと解釈すると、@Bindableは以下に該当する場合に使用すると考えられそうです。

  • サブビューであること
  • View内でモデルのプロパティをBindingで渡す必要がある
  • モデルが@Observableであること

以下がWWDCの動画で示されたコード例です。

@Bindableの使用ケース
@Observable class Donut {
  var name: String
}

struct DonutView: View {
  @Bindable var donut: Donut

  var body: some View {
    TextField("Name", text: $donut.name)
  }
}

DonutView@Stateの使い所のコード例ででたDonutListViewのサブビューであり、かつTextFieldBindingを使用しています。
また、Donut@Observableなオブジェクトなので、以下条件を満たしていると言えます。

  • サブビューであること
  • View内でモデルのプロパティをBindingで渡す必要がある
  • モデルが@Observableであること

@Binding@Bindableてどう違う?

個人的に結構悩んだところなので、ついでに自分で調べた内容を紹介しておきます。
自分が調べた限り、違いは以下2点です。

  • 参照自体の変更を追うかどうか
  • プロパティ定義以外でも使えるかどうか
  • Observableprotocolに準拠している必要があるかどうか

参照自体の変更を追うかどうか

@Bindingの場合は参照の変更自体を検知することができます。
そのため、以下のようにOptional型も指定可能で、bookのインスタンスを入れ替えることもできます。
@Bindableではこれは行えません。

struct DeleteBookView: View {
    @Binding var book: Book?
    
    var body: some View {
        Button("Delete book") {
            book = nil
        }
    }
}

これはSwiftUIのドキュメントコメントにて記載がありますが、長いので折りたたみで表示しておきます。

SwiftUIのドキュメントコメント
/// ### Share observable state objects with subviews
///
/// To share an <doc://com.apple.documentation/documentation/Observation/Observable>
/// object stored in `State` with a subview, pass the object reference to
/// the subview. SwiftUI updates the subview anytime an observable property of
/// the object changes, but only when the subview's ``View/body`` reads the
/// property. For example, in the following code `BookView` updates each time
/// `title` changes but not when `isAvailable` changes:
///
///     @Observable
///     class Book {
///         var title = "A sample book"
///         var isAvailable = true
///     }
///
///     struct ContentView: View {
///         @State private var book = Book()
///
///         var body: some View {
///             BookView(book: book)
///         }
///     }
///
///     struct BookView: View {
///         var book: Book
///
///         var body: some View {
///             Text(book.title)
///         }
///     }
///
/// `State` properties provide bindings to their value. When storing an object,
/// you can get a ``Binding`` to that object, specifically the reference to the
/// object. This is useful when you need to change the reference stored in
/// state in some other subview, such as setting the reference to `nil`:
///
///     struct ContentView: View {
///         @State private var book: Book?
///
///         var body: some View {
///             DeleteBookView(book: $book)
///                 .task {
///                     book = Book()
///                 }
///         }
///     }
///
///     struct DeleteBookView: View {
///         @Binding var book: Book?
///
///         var body: some View {
///             Button("Delete book") {
///                 book = nil
///             }
///         }
///     }
///
/// However, passing a ``Binding`` to an object stored in `State` isn't
/// necessary when you need to change properties of that object. For example,
/// you can set the properties of the object to new values in a subview by
/// passing the object reference instead of a binding to the reference:
///
///     struct ContentView: View {
///         @State private var book = Book()
///
///         var body: some View {
///             BookCheckoutView(book: book)
///         }
///     }
///
///     struct BookCheckoutView: View {
///         var book: Book
///
///         var body: some View {
///             Button(book.isAvailable ? "Check out book" : "Return book") {
///                 book.isAvailable.toggle()
///             }
///         }
///     }
///
/// If you need a binding to a specific property of the object, pass either the
/// binding to the object and extract bindings to specific properties where
/// needed, or pass the object reference and use the ``Bindable`` property
/// wrapper to create bindings to specific properties. For example, in the
/// following code `BookEditorView` wraps `book` with `@Bindable`. Then the
/// view uses the `$` syntax to pass to a ``TextField`` a binding to `title`:
///
///     struct ContentView: View {
///         @State private var book = Book()
///
///         var body: some View {
///             BookView(book: book)
///         }
///     }
///
///     struct BookView: View {
///         let book: Book
///
///         var body: some View {
///             BookEditorView(book: book)
///         }
///     }
///
///     struct BookEditorView: View {
///         @Bindable var book: Book
///
///         var body: some View {
///             TextField("Title", text: $book.title)
///         }
///     }
///

プロパティ定義以外でも使えるかどうか

@Bindableは型のプロパティだけでなく、関数やコンピューテッドプロパティのスコープでも使用できます。
@Bindingではこれはできません。
用途として、ListForEach内でBindingを使用したい場合は便利です。

@Observable
final class SampleStore {
    var children: [SampleChildStore] = [
        .init(),
        .init()
    ]    
}

@Observable
final class SampleChildStore: Identifiable {
    var id: ObjectIdentifier { ObjectIdentifier(self) }
    var value: String = ""
}

struct BindSampleView: View {
    @Bindable var store: SampleStore
    
    var body: some View {
        VStack {
            ForEach(store.children) {
                @Bindable var child = $0 // コンピューテッドプロパティのスコープでも使用可能
                TextField("child value", text: $child.value)
            }
        }
        .padding()
    }
}

以下記事で紹介されていました。

Observableprotocolに準拠している必要があるかどうか

これはタイトル通りなので、それほど説明することはありませんが、
@Binableを付与できる型はObservableに準拠している必要があります。
@Bindingはそのような制約はありません。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?