細かいことですが、SwiftUIなどで時折書く$hoge.fuga
という式は$(hoge.fuga)
ではないのだ、という話をします。
例
まずは記述例を挙げます。
struct Hoge {
var fuga: String = ""
}
struct MyContentView: View {
@State private var hoge = Hoge()
var body: some View {
TextField("入力してね", text: $hoge.fuga)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
非常にシンプルです。
では、次のように書いてみましょう。エラーが出ます。
//Error: '$' is not an identifier; use backticks to escape it
TextField("入力してね", text: $(hoge.fuga))
それからこれもダメです。
//Error: Referencing subscript 'subscript(dynamicMember:)' requires wrapper 'Binding<Hoge>'
//Error: Value of type 'Hoge' has no dynamic member '$fuga' using key path from root type 'Hoge'
TextField("入力してね", text: hoge.$fuga)
これでひとまず$hoge.fuga
が$(hoge.fuga)
でないことはおわかりいただけたと思います。しかしでは$hoge.fuga
とはなんなのでしょう。
正体
そもそも$
を用いたアクセスはpropertyWrapper
の機能によるものです。State<T>
のprojectedValue
はBinding<T>
となっています。ですから$hoge
とはBinding<Hoge>
の値です。
しかし、そうするとBinding<Hoge>
にプロパティfuga
が存在するのが不思議に思えるはずです。これを実現しているのがdynamicMemberLookup
です。Swift5.1以降でKeyPath
を使ったLookupができるようになったため、おそらくBinding
が次のように定義されています。
@dynamicMemberLookup
struct Binding<T> {
subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Binding<U> {
//処理
}
}
これだけでは少しわかりにくいので、もっと単純なサンプルを見てみましょう。
@dynamicMemberLookup
private struct Wrapper<T> {
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}
private var wrappedValue: T
subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> Wrapper<U> {
get {
return Wrapper<U>(wrappedValue: wrappedValue[keyPath: keyPath])
}
}
}
こうすると、value
はWrapper
型の値であるにもかかわらず、String
が持っていたはずのプロパティにアクセスできるようになっています。もちろん定義した通りWrapped<Int>
の値です。
以上で$hoge.fuga
の正体は説明がつきます。$hoge
がState
というpropertyWrapper
のprojectedValue
であり、その値はBinding<Hoge>
型です。Binding<Hoge>
型はdynamicMemberLookup
をサポートしており、これを通してhoge
のプロパティであるfuga
をBinding<String>
に変換して返すのです。
補足
以上だけだとひょっとすると$hoge[0]
のような記法が可能なことが奇妙に思えるかもしれません。実際以下は全く正しく動きます。
struct MyContentView: View {
@State private var hoge = ["A", "B", "C"]
var body: some View {
TextField("入力してね", text: $hoge[0])
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
実はこれもdynamicMemberLookup
で実現されています。びっくりするほど気持ち悪いですが、subscript
へのKeyPath
というものが存在するおかげです。
let keyPath: WritableKeyPath<[Int], Int> = \.[0]
以上でだいたい網羅できたのではないかと思います。
参考