SwiftUIの「$」って何ですか?


$ってなに

よくこのようにTextFieldなどで、stateプロパティに「\$」をつけたものを渡します。
「$」をつけると値の変更を検知してプロパティを更新してくれるようなる
と何となく理解していましたが、理解が不十分なので調べてみました。

@State private var text1 = ""

var body: some View {
    TextField("text1", text: $text1)
}

公式ドキュメントを読んでみる

まずは公式ドキュメントに何か書いてないかと思い
Stateのドキュメントを確認してみました。

To pass a state property to another view in the view hierarchy, use the variable name with the \$ prefix operator. This retrieves a binding of the state property from its projectedValue property.

Viewの階層下にある他のViewにstateプロパティを渡すには、「$」を変数の先頭つけて使う。
これはそのprojectedValueからstateプロパティのbindingを取得する。

太文字部分が「$」をつけてできることを示していることはわかりましたが
またわからないことが出てきました。

  • projectedValueってなに

A binding to the state value.

StateのprojectedValueを一言で表すと
"state valueへのbinding" のようです。
getterプロパティとして用意されています。

var projectedValue: Binding<Value> { get }
  • Bindingってなに

A property wrapper type that can read and write a value owned by a source of truth.

一言で表すと "信頼できる一つの情報源に所有された値を読み書きできるプロパティラッパータイプ" のようです。
難しいのでもう少し読んでみます。

Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data

データを格納するプロパティとViewを表示したり更新したりするデータの間の双方向接続を作るためにbindingを使用する

BindingがプロパティとUIの双方向の接続のためのものであることがわかりました。

つまり
「\$」をつけるとprojectedValueというgetterプロパティへアクセスすることになる。
Stateで定義したプロパティのprojectedプロパティは
stateプロパティのbindingを取得することになるので
今回の例の場合 $text は Binding<String> を表しています。

TextFieldで実際に確認してみた

TextFieldを使って確認していきます。
TextFieldの第二引数にはBinding<String>を渡さなければいけないため
stateプロパティをそのまま渡すとエラーになります。
TextFieldはユーザーの入力を受け付けるので
入力された値とプロパティの値が同期されるように
Bindingでラップした値を入れる必要があるのは理解できますね。

@ObservedObject@Publishedプロパティの場合どうなる?

もう少し深掘りしていきます。
今度は@ObservedObject@Publishedの値をTextFieldに渡す場合に
どこに「\$」を付けるべきなのかを確認していきます。
4つの選択肢のうちどれが正解でしょうか?

$model.text  - ①
$model.$text - ②
model.$text  - ③
model.text   - ④

私は@Published のprojectedValueが 値をBindingでラップした値を返すと考え
それがBindingになると思ったので③model.$textかなと思いましたが違っていました。

正解は①$model.textでした。

他の選択肢はエラーが出たので確認していきます。

③model.$text

予想していたこれからみていきます。
Cannot convert value of type 'Published.Publisher' to expected argument type 'Binding'
'Binding'ではなく'Published.Publisher'になっているようです。
公式ドキュメント確認すると
@PublishedのprojectedValueはPublished<Value>.Publisherでした。
そのため model.$text は Published.Publisher を表しており、不正解です。

②$model.$text

Cannot convert value of type 'Binding.Publisher' to expected argument type 'Binding'
'Binding.Publisher'になっています。
@ObservedObjectのprojectedValueを確認すると
ObservedObject<ObjectType>.Wrapperになっていましたがパッとみてわかりません。
説明文を読むとこうありました。

A projection of the observed object that creates bindings to its properties using dynamic member lookup.

理解しづらいですが私は
保持しているプロパティのBindingを動的に生成する監視オブジェクトの投影
つまり
複数のプロパティを持つBindingだと飲み込みました。

そのため \$model.$text は Binding<Published.Publisher> を表しており、不正解です。

④model.text

これは単純にStringを表しているので不正解です。

@ObservedObjectをネストさせたら・・・

さらに@ObservedObjectの中に@ObservedObjectを入れてみたらこの2通りがエラーになりませんでした。

TextField("text", text: $model.model.text)
TextField("text", text: model.$model.text)

ただビルドして入力してみるとこのようなwarningが出ました。
Binding action tried to update multiple times per frame.
おそらく上記のような使い方はしないほうがよさそうです。
これに関しては別記事にしたいと思います。

次に記事にしたいこと

  • ObservedObjectをネストした場合の「$」の使い方
  • SwiftUIの「@」って何ですか?