久しぶりの記事です。
ついに?SwiftUIでUIを書き始めて思うことがあったので書きます。
SwiftUIで意味のあるアプリを書こうとすると、@Stateや@Binding、@StateObject/@ObservedObject, @ObservableObject,@EnvironmentObjectと色々なプロパティラッパが出てきます。
皆さんこのラッパのネーミングに釣られて、UIのちょっとしたフラグはストラクトでStateに、ビューで表示したいデータはクラスで書き始めませんでしたか?
私はそうしました。Swiftはストラクト/プロトコル指向言語なのになんで?と思いながら。
どっぷり使うことになるだろうフレームワークを「なんで?」を抱えながらコーディングすると筋の悪いことをたくさんし始めて碌なことになりません。
で、私なりに考え直しました。
以下がそのメモです。
- モデルはロジック設計に任せる。SwiftUIの方言(プロパティラッパ)を絶対入れてはいけない。
- ビューに表示したいデータはビューモデルを別途作成する。この時必要にならない限りクラスではなくストラクトで作成する。
- モデルが更新されるとビューモデルが更新されるようにコーディングする。この実装には様々な方法がある。
- (ビューモデルにビューが1つなら)ビューには@State(ビューモデルがクラスの場合@StateObject)でビューモデルを持たせる。ビューのinitにモデルを渡してビューモデルを初期化するようにする。この時モデルをビューのプロパティにしないようにする。モデルとのやり取りの責務はビューモデルが担当する。
- 子ビューのユーザーイベントでモデルを変更したい時、子ビューに@Bindingで何かを渡さない。代わりにビューモデルにモデルを変更するロジックを書いて、そのクロージャをイベントアクションとして子ビューに渡す。
これでシンプルなアプリの場合には、クラスを使わずにビューモデル、ビューを記述できるはず。
なので@Binding, @StateObject/@ObservedObject, @ObservableObjectの出番はなくなります。
ただ、ビューの階層構造が深いとイベントアクションの子ビュー孫ビューひ孫ビュー...バケツリレーが面倒になるので、@EnvironmentObjectを使いたくなるケースが出てくるかもしれません。
でもできるだけ避けるほうがよいと思っています。@EnvironmentObjectを使うとその時点でビューが再利用可能でなくなりますから。
(イベントアクションについてはThe Composable Architectureにインスパイヤされました。勉強してないので、この記事が書くまでもなくTCAそのものかもしれないし、この記事が全く別のパチモンかもしれません)
もう1つ、実は2の「必要にならない限り」という条件が意外と簡単に破れるケースがあります。
それはモデルがエスケーピングクロージャーを使っている場合です。
ストラクトは、実行時に存在が保証できないのでエスケーピングクロージャーに参照型(inout)で渡すことができません。
つまり、モデルが非同期に更新された時、それをハンドラ(エスケーピングクロージャー)でストラクトのビューモデルに反映させることはできません。
この場合、ビューモデルをObservableObjectとしてクラスで設計する必要があります。
今回の主旨は、「プロパティラッパの名前に釣られて安易にクラスに流れるな」「クラスを使わずストラクトでもSwiftUIは結構書ける」でした。