MVVMに慣れた人がSwiftUIでもMVVMを採用しようとすると、ModelとViewModelをどう繋ぐのか?という点で悩むと思います。 本記事では、そんな悩みを解決します。
SwiftUIでMVVMを適用する際の悩み
まずは悩みを再確認しておきましょう。
SwiftUIでMVVMを適用しようとすると、View/ViewModel/Modelはそれぞれ下記のように実装しようと考えると思います。
要素 | 実装方針 |
View | View |
ViewModel | ObservableObject |
Model | Class |
これはとても自然でうまく適用できるように感じます。
ViewとViewModelの間は、Bindingは@Publishで行えますし、Commandは単にメソッド呼び出しでよいでしょう。
ViewModelとModelの間も、Commandは同様にメソッド呼び出しでいいですし、Notificationは例えばdelgateを使えば良さそうです。
ViewとViewModelの間についてはその通りで何も問題はありません。しかし、ViewModelとModelの間のNotificationに落とし穴があります。
まず、単なるdelegateにするとうまくいかないケースが多いことに気が付くはずです。なぜなら、たいていのケースでは、Model1つに対し複数のViewModelが通知を受け取りたいからです。
次に思いつくのは、Model側にaddDelegate()メソッドを用意して複数のdelegateに通知できるようにする方法です。これであればうまくいきそうですが、この方法も注意して実装しないとメモリリークの可能性があります。
ViewModelの初期化の際にaddDelegate()を呼び出すことになると思います。そしてViewModelはViewの初期化と合わせて生成されることが多いと思います。例えば、こんな感じです。
struct SampleView: View { @ObservedObject var vm = SampleViewModel() : } class SampleViewModel: ObservableObject, SampleModelDelegate { @Publish var text = "" init() { SampleModel.instance.addDelegate(self) } }
コードだけ見ると何も問題ないように思いますが、このままだとメモリリークする可能性があります。
理由はSwiftUIのViewのライフサイクルにあります。Viewはバインディングしているオブジェクトに変更があった際、内部状態を変えて表示を更新するのではなく、View自身を再度作り直すのです。
そのため、画面更新のある度にViewModelも一緒に作り直されて、Model側にdelegateのリストが増加していってしまうのです。
SwiftUIでViewModelとModelを繋ぐ方法
この悩みの解決策として、Model側でdelegateをDictionaryに格納する方法を提案します。
Dictionaryにdelegateを格納し、そのキーにはViewModelを識別する情報を使用します。もし、そのViewModelクラスが1つしか作られないのであればクラス名をそのままキーに使えばよいでしょう。例えばこんな感じです。
class SampleViewModel: ObservableObject, SampleModelDelegate { @Publish var text = "" init() { SampleModel.instance.setDelegate(String(describing: type(of: self)), self) } }
こうしておけば、ViewModel(View)が再生成されても、同じキーで上書きされるだけなので、Dictionaryの中のdelegateが無限に増えていくことはなくなります。
ViewModelクラスが複数インスタンス化される場合は、キーを工夫すれば大丈夫です。例えば、こんな感じです。
class SampleViewModel: ObservableObject, SampleModelDelegate { @Published var name: String @Publish var text = "" init(name: name) { self.name = name SampleModel.instance.setDelegate(self.name + "_" + String(describing: type(of: self)), self) } }
とてもシンプルな解決策です。ぜひ参考にしてみてください。
この解決策では対処できない場合
この解決策でたいていのケースは問題ありませんが、うまく適用できないケースもあるので説明しておきます。
うまく適用できないのは、ViewModelのキーの数に上限がなく、いくらでも増えていってしまうケースです。例えば、ユーザのリストを表示するアプリケーションで、ユーザごとにViewModelを作るような場合です。ユーザが増えればそれだけキーも増えるので、その分delegateも増えていってしまいます。
その際は、そのViewModelが本当に通知を受け取る必要があるのか検討してみてください。おそらくSwiftUIのListで繰り返しViewを表示するようなものだと思いますが、そのListを表示する親のようなViewのViewModelだけが通知を受け取れれば十分だったりしないでしょうか?
たいていのケースでは親のViewだけで十分だと思います。
もし必要なときのことについては、また別の機会に書きたいと思います。