LoginSignup
2
1

More than 3 years have passed since last update.

[SwiftUI] $hoge.fugaは$(hoge.fuga)ではない

Last updated at Posted at 2021-04-10

細かいことですが、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>projectedValueBinding<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])
        }
    }
}

こうすると、valueWrapper型の値であるにもかかわらず、Stringが持っていたはずのプロパティにアクセスできるようになっています。もちろん定義した通りWrapped<Int>の値です。

以上で$hoge.fugaの正体は説明がつきます。$hogeStateというpropertyWrapperprojectedValueであり、その値はBinding<Hoge>型です。Binding<Hoge>型はdynamicMemberLookupをサポートしており、これを通してhogeのプロパティであるfugaBinding<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]

以上でだいたい網羅できたのではないかと思います。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1