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
の使い所に悩んでしまいました。
@State
から学ぶ Property Wrapper
SwiftUI の 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