概要
SwiftUIでは、変数を参照型として扱うためのBindingという型があります。
Bindingを扱うためには、@Binding
という PropertyWrapperを使うことが多いと思いますが、Binding型
というデータ型と@Bindingの扱いでたまに混乱してしまうことがあります。
そのため、整理のための @Binding と Binding型について自分なりにまとめてみました。
自分が混乱しがちなことでもあり、誤った解釈があるかもしれませんので、お気づきの方がいればご指摘いただければ幸いです。
環境
- Swift: 5.2.2
- Xcode: 11.4.1
@Binding
@Bindingは、Binding型を扱うための PropertyWrapper です。
一般的な使い方としては、別のViewに対して参照型ではない値(Bool型など)を渡す場合に使われることが多いかと思います。
例えばモーダルのViewを表示するための .sheet
というModifireでは、表示/非表示の制御のために Binding<Bool>
の値を使っています。
■ モーダルを呼び出すときのサンプル
import SwiftUI struct ContentView: View { @State var isPresented: Bool = false var body: some View { VStack { Button(action: { self.isPresented = true }) { Text("モーダルを表示") } .sheet(isPresented: $isPresented) { ModalView(isPresented: self.$isPresented) } } } }
sheet
Modifireに渡している isPresented
という値が、Binding型の値です。
PropertyWrapperで$
をつけた値は、射影プロパティ
と呼ばれますが、 @State
は 射影プロパティ
としてBinding型の値を返します。
この例では、ModalView
にも $isPresented
を渡しています。
このBinding型の値isPresentedを受け取るために ModalViewでは @Binding
というPropertyWrapper を使います。
■ モーダルViewのサンプル
struct ModalView: View { @Binding var isPresented: Bool var body: some View { VStack { Button(action: { self.isPresented = false }) { Text("モーダルを閉じる") } } } }
このように、@Binding
をつけた変数を持つことでBinding型の値を受け取ることができます。
Binding型として、isPresented
を受け取ることで ModalView
内部からも ContentViewが持っている変数を変更できるようになるというのが利点です。
この例では、ModalView内でisPresented
をfalseに変更して画面を閉じる処理を実装しています。
要するに @Binding
は、指定のプロパティの型を Binding<型>
として受け取るため使われる PropertyWrapperです。
@BindingのプロパティにBinding型の変数を格納する
@Binding
は、Binding型を扱うためのPropertyWrapperですが、このプロパティに Binding型の変数を格納するにはどうすればいいのでしょうか?
上記の例では、struct
が自動生成するイニシャライザに代入処理を任せているため、意識していないですが、自分でイニシャライザを作る場合は次のように記述します。
init(isPresented: Binding<Bool>) { self._isPresented = isPresented }
受け取る引数は、Bindig型の変数で宣言して、プロパティに代入するときは、 _isPresented
というプロパティ名に アンダースコア(_
)をつけたものに代入しています。
init(isPresented: Binding<Bool>) { self.isPresented = isPresented }
このように書いてしまうと Cannot assign value of type 'Binding<Bool>' to type 'Bool'
というコンパイルエラーになってしまいます。
isPrerented
のプロパティは、Boo型なのでBinding<Bool>
とは型が異なるためです。
このように PropertyWrapperを定義したプロパティに(_)をつけると PropertyWrapperの内部変数にアクセスできます
。
つまり @Binding var isPresented: Bool
の場合は、@Bindigが管理してくれている Binding<Bool>
にアクセスするために(_)をつける必要があります。
余談ですが、$
をつけると射影プロパティにアクセスできますが、これは内部変数(_をつけた変数
)から projectedValue
というプロパティへアクセスするのと等価になります。
以下の構文は等価です。
_isPresented.projectedValue $isPresented
@Bindingを使わない場合の例
先程のModalViewのサンプルコードを、@Bindingを使わずにBinding型の変数を使って実装することも可能です。
struct ModalView: View { var isPresented: Binding<Bool> init(isPresented: Binding<Bool>) { self.isPresented = isPresented } var body: some View { VStack { Button(action: { self.isPresented.wrappedValue = false }) { Text("モーダルを閉じる") } } } }
この場合のイニシャライザは、引数とプロパティの方はともにBinding<Bool>
のため直接代入できます。
異なる点は、「値の更新方法」でBinding型から内包している値へアクセスするために wrappedValue
というプロパティを使います。
@Bindingを使って値を更新する場合
@Binding var isPresented: Bool ... self.isPresented = false
Binding<変数>を使って値を更新する場合
var isPresented: Binding<Bool> ... self.isPresented.wrappedValue = false
このように、Binding型を直接プロパティとして持つことは可能です。
しかし、実践上は @Bindingを使ことでBindingされていないデータ型と同じ記述で参照/更新を行えるため、特別な理由がない限りは@Bindingで受け取るのが良いかと思います。
Binding型のデータの値変更の注意点
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) @frozen @propertyWrapper @dynamicMemberLookup public struct Binding<Value> { ... /// The value referenced by the binding. Assignments to the value /// will be immediately visible on reading (assuming the binding /// represents a mutable location), but the view changes they cause /// may be processed asynchronously to the assignment. public var wrappedValue: Value { get nonmutating set } ... }
Binding型で内部の値を変更するには、wrappedValueを使いますが、wrappedValueの定義には上記のような記載があります。
このコメントによると、wrappedValueへの代入による変更は 非同期で行われる場合がある
とのことです。
nomutating
がセッターについていることからも直接代入により値を変更するわけではないようです。
簡単なサンプルで確認してみます。
struct ContentView: View { @State var num: Int = 1 var body: some View { VStack { SecondView(num1: $num) } } } struct SecondView: View { @Binding var num1: Int var body: some View { return VStack { Button(action: { self.num1 += 1 print(self.$num1.projectedValue) print(self.num1) }) { Text("count up") } } } }
このサンプルでは、Binding<Int>
型の値をSecondViewで受け取り、ボタン操作によって値を増加させています。
print(self.num1)
ではゲッターにより値を直接参照していて、print(self.$num1.projectedValue)
は Binding<Int>
オブジェクトの中身を表示しています。
このボタンを押下するとコンソールは以下のように出力されます。
■ print(self.num1)
2
■ print(self.$num1.projectedValue)
Binding<Int>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.LocationBox<SwiftUI.Binding<Swift.Int>.(unknown context at $7fff2c9b3008).ScopedLocation>, _value: 1)
getter自体は即時2を返していることがわかります。
しかし、Bindingオブジェクトの_value
は1のままのようです。
これを見ると、値セット時に直接値を変更しているわけではないということが確認できます。