[Swift] PropertyWrapper 手を動かして理解する(その1 wrappedValue)
[Swift] PropertyWrapper 手を動かして理解する(その2 wrappedValue の初期化)
[Swift] PropertyWrapper 手を動かして理解する(その3 $ もしくは projectedValue の理解)
Sponsor Link
@State, @Binding の定義
property wrapper としての定義を見てみます。
@State の定義
@State var value:Int とすると
wrappedValue : value
projectedValue: Binding<value>
@Binding の定義
@Binding var value:Int とすると
wrappedValue : value
projectedValue: Binding<value>
プチまとめ
@State と @Binding の違い
property wrapper 視点での interface 的には、違いはありません。
ですが、Binding では、transaction という情報を持つことで、変化についての情報を保持することができるようになっています。
Transaction についての Apple のドキュメントは、こちら。
本題ではないので、説明は省きます。
MyState, MyBinding を作って使ってみる
MyState, MyBinding を作ってみる
Property wrapper の仕組み自体は、Swift 言語の仕様の一部ですので詳細が開示されています。
ですが、@State や @Binding は、SwiftUI の一部であり、使うことはできますが、実装の詳細は開示されていません。
Apple の説明で、@State は、source of truth であり、@Binding はあくまで、@State への reference であるというような説明がされているかと思います。
そこで、@MyState と @MyBinding を以下のように定義しました。
class Storage {
var data:Value
init(data:Value) {
self.data = data
}
}
@propertyWrapper struct MyState {
var internalStorage: Storage // 実際のデータ保存場所の定義 クラスで定義されているので、保存されたデータへのポインタが保持されている
var wrappedValue: Value { // get されるときには、保存場所にあるデータを返す
get {
return self.internalStorage.data
}
nonmutating set {
self.internalStorage.data = newValue // set されるときには、保存場所は変えずに、保存されているデータを更新する
}
}
init(wrappedValue:Value) {
self.internalStorage = Storage(data: wrappedValue) // 初期化されるときに、データ保存場所を作成する
}
var projectedValue: MyBinding { // データ保存場所へのリファレンスを保持する MyBinding を返す
get {
return MyBinding(wrappedValue: self.wrappedValue, storage: self.internalStorage)
}
}
}
コード上にコメントを入れていますが、データの保存場所は、MyState の外部にあり、そこへの参照を保持します。
値をセットされるときには、データの保存場所にあるデータを書き換えるため、MyState という struct 自身の変更は発生しません。
projectValue としては、データ保存場所へのリファレンス MyBinding を返すようにしています。
@propertyWrapper struct MyBinding {
var sharedStorage:Storage // 外部から渡された保存場所へのリファレンスを保持
var wrappedValue: Value {
get {
return self.sharedStorage.data // get されるときは、保存場所にあるデータを返す
}
nonmutating set {
self.sharedStorage.data = newValue // set されるときは、保存場所は変えずに、保存されているデータを更新する
}
}
init(wrappedValue: Value, storage: Storage) { // 初期化されるときに、外部から保存場所へのリファレンスを渡してもらう
self.sharedStorage = storage
}
var projectedValue: MyBinding { // データ保存場所へのリファレンスである自身を返す
get {
return self
}
}
}
MyState, MyBinding を使ってみる
以下のようなアプリを作って動作を見てみます。
MyStateMyBinding を使ったアプリ
以下が、ソースコード です。
struct ContentView: View {
@EnvironmentObject var dummy:DummyObject
@MyState private var value:Int = 0
var currentValue:Int = 0
var body: some View {
VStack {
DisplayView(value: value)
.padding()
EditView(value: $value)
.padding()
Button(action: {
self.dummy.refresh()
}, label: { Text("refresh label")})
.padding(3)
.border(Color.green, width: 3)
}
}
}
struct DisplayView : View {
@EnvironmentObject var dummy:DummyObject
var value:Int
var body: some View {
Text("\(value)")
.padding(3)
.border(Color.orange, width: 3)
}
}
struct EditView : View {
@EnvironmentObject var dummy:DummyObject
@MyBinding var value:Int
var body: some View {
Stepper("stepper \(value)", onIncrement: {
self.value = self.value + 1
}, onDecrement: {
self.value = self.value - 1
})
.padding(3)
.border(Color.red, width: 3)
}
}
MyState, MyBinding には、画面のリフレッシュを行う仕組みを入れていないために、ダミーの ObservableObject を使ってリフレッシュするようにしています。
class DummyObject: ObservableObject {
@Published var dummyVar:Int = 0
func refresh() {
self.objectWillChange.send() // DummyObject の refresh を呼ぶことで、画面更新
}
}
@main
struct UnderstandStateApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(DummyObject())
}
}
}
@State, @Binding を使っていませんが、Stepper の + ボタン、- ボタンを押下して、”refresh label” ボタンを押下すると、値が変更されていることがわかります。
# あくまで、機能性として近いものを実装したということで、Apple の実装とどれくらい近いかは不明です。例えば、値の更新について伝播させる仕組み等が入っていません。
@State という property wrapper では、 source of truth の場所を確保しデータを保存するようにしています。その上で projectValue として、その場所へのリファレンスを渡します。@Binding は、その保存場所へのリファレンスを保持する property wrapper で、保存場所へアクセスすることで、直接保存場所の値を変更することができます。
まとめ
イメージとしては、以下のようなイメージです。
Image: @State, @Binding
- @State は、source of truth を作り出す
- @Binding は、source of truth へのリファレンスを保持する
- @Binding 経由で source of truth を直接変更することができる
- (補足) @State, @Binding で行っている変更の伝播を実装するのは大変そう。
説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。
Sponsor Link