SwiftUI の @State は Property Wrapper としてどのように表現されているのか
Swift の Language Guide の Properties を見て Property Wrappers を勉強していたのですが、Property Wrappers 自体については理解できたものの、実際にどのように使うかというイメージが湧かずでした。
ちょうど SwiftUI では @State という Property Wrapper があるので、それがどのように Property Wrapper として実現されているのかを見れば使い方をイメージできそうと思いました。
実際、@State について見てみるとある程度 Property Wrapper の旨味が理解できた気がしたので、それについて書こうと思います。
Language Guide から学ぶ Property Wrapper
Language Guide 自体に詳しい Property Wrapper についての説明はあるため、ここでは最小限のコードの紹介に留めます。
Language Guide 内では、以下のような形で Property Wrapper の使い方が紹介されていました。
@propertyWrapper
struct SmallNumber {
private var number: Int
private(set) var projectedValue: Bool
var wrappedValue: Int {
get { return number }
set {
if newValue > 12 {
number = 12
projectedValue = true
} else {
number = newValue
projectedValue = false
}
}
}
init() {
self.number = 0
self.projectedValue = false
}
}
struct SomeStructure {
@SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()
someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false"
someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"
大きく、Property Wrapper には wrappedValue と projectedValue というものがあります。
それぞれ簡単に以下のようなものだと理解しています。
-
wrappedValue: 名前の通りラップされた値。↑ のコードの例のように、何らかの条件に基づいて値に制限を付けたい時などに利用できそう -
projectedValue: property に追加の機能を付与することができるもの(↑ の例だと、独自のバリデーションに引っかかったか否かを Bool 値として公開している)。$プロパティ名という形でアクセスできる
個人的には ↑ にある例だと wrappedValue の旨味はなんとなく理解できたのですが、projectedValue についてはどうやって使うべきなのかと悩んでしまいました。
具体的には、projectedValue 的な何かの値をもとにした値(例えば validation の結果など)については、特定のアーキテクチャパターンに沿って開発を進めていれば、property として表現するのではなく、別の class 内に閉じ込めて責務を分離したいと思うだろうな〜と感じたため、projectedValue の使い所に悩んでしまいました。
SwiftUI の @State から学ぶ Property Wrapper
projectedValue をどう使うべきか悩んでいた時に、「そういえば SwiftUI の @State って projectedValue 使ってるな」と思い出しました。
そこで、@State が Property Wrapper としてどのように実現されているのか探れば、Property Wrapper についての理解が深まるかなと思い、調べてみることにしました。
SwiftUI の @State は例えば以下のような形で利用すると思います。
struct ContentView: View {
@State private var name: String = "Bob and Alice"
var body: some View {
VStack {
Text("Hello, \(name)!")
TextField("Name", text: $name)
}
}
}
@State は Property Wrapper なので、wrappedValue と projectedValue がそれぞれ何なのかを考えてみると、以下のようになると思います。
-
wrappedValue:Text("Hello, \(name)!")として利用されているname -
projectedValue:TextField("Name", text: $name)として利用されている$name
これの何が嬉しいかを少し説明しようと思います。
まず wrappedValue についてです。
wrappedValue については、name という形で利用されており、これによって "Bob and Alice" という String を得ることができます。
これについては Property Wrapper を利用しないただの String と大差はないと思います。
次に projectedValue についてです。こちらは $name という形で利用されています。
projectedValue としてアクセスする $name は "Bob and Alice" という String を得ることができるわけではなく、Binding<String> という型を得ることができます。
TextField は init(_ title: S, text: Binding, ...) という形で利用できます。
つまり、projectedValue として name にアクセスすることによって TextField が必要としている Binding の定義を満たせるという仕組みになっていると考えられます。
SwiftUI を利用していると何気なく使えてしまう @State ですが、Property Wrapper によって意識せずに値そのものを利用したり、値を Binding という型で利用できることがわかりました。
使いこなすのは難しそうではありますが、@State 的な形でスマートに使えたら中々役立ちそうだなと感じました。
参考
- Properties
- State
- The @State Property Wrapper in SwiftUI Explained
Discussion