はじめに
以前書いた今更MVCとかでiOSアプリつくってみた(Swift)・改の続きです。なんか面談のときにちょこちょこアーキテクチャについて聞かれたので今度は MVVM について考えてみます。
前記事でも書きましたが、アーキテクチャについてネットで色々検索すると言ってることがそれぞれ微妙に違うのがいっぱい出てきます。アーキテクチャについて学びたいと思うなら iOSアプリ設計パターン入門 を読みましょう!
この記事は iOSアプリ設計パターン入門 を参考にしていますが、あくまで私個人の意見です。
わりと長くなったのでめんどくさい人はソースだけでもどうぞ
長いの読みたくない人ように結論だけ
たぶん MVVM で一番重要なのは VM が V に依存してなくてプレゼンテーションロジックのテストがしやすいこと!!
MVVM について
2005年に WPF、Silverlight プラットフォーム向けに MVVM が誕生
Presentation Model の MVC (プレゼンテーションロジック担当に V, C と M の間に Presentation Model を置いたやつ)を上記プラットフォーム向けに特殊化し、プレゼンテーションロジックを担うやつとして ViewModel を置いたもの。
WPF、Silverlight では V のテンプレートを XAML(XMLベースのDSL)で宣言し自動的に V と VM をバインドする機能があり、MVVM はこの機能をふんだんに使ったものだった。
各役割は以下のような感じ
- View
入出力。VMとバインディング。 - ViewModel
プレゼンテーションロジック。V と M の仲介。 - Model
UI 関係以外のやつ。MVC、MVPと同じ。完全独立。
* 詳細は第 4 章 アーキテクチャのパターンを鳥瞰するの 4.2.3 Model-View-ViewModel (MVVM)
iOS における MVVM
iOSには WPF、Silverlight のような V, VM を自動でバインディングする機能はないので、ReactiveSwift や RxSwift を利用する必要がある。(もしかしたら SwiftUI + Combine でもできるかもしれない)
なぜ MVVM を導入するのか?
iOS で MVVM を導入する理由は下記3点だと思います。
- プレゼンテーションロジックのテストするため
- FatViewController を防止するため
- 関数型リアクティブプログラミング(FRP)と相性がいいため
これだけ他の理由とはちょっと違います。iOSの場合、RxSwift などを導入して FRP を実現したい場合に相性のいい MVVM を採用するというのがほとんどらしい。(つまり FRP やりたい! -> じゃあ MVVM でいいよねって流れが多いのかと思う)
上記について(今回は3の理由は無視します) Cocoa MVC の場合 (以降、MVC は Cocoa MVC のことを指します。)
- プレゼンテーションロジックのテストするため
C にプレゼンテーションロジックがあるのでテストは困難。 - FatViewController を防止するため
C が M と V の仲介をし、プレゼンテーションロジック担っているので太りやすい。
これらを解決するために MVVM を導入すると考えています。(個人の意見です。)
なんとなく MVPと同じ感じがしますが、MVP は P が V に依存しており、MVVM は VM が V に依存していないのが大きな違いかと思います。
お天気アプリ実装
MVVM の考え方をわかった気になったところでいよいよ実装にはいっていきます。
V と VM のバインディングには ReactiveSwift を利用しました(SwiftUI + Combine も挑戦しようと思いましたが挫折しました。。。)
Cocoa MVC でつくったやつを MVVM に分けていきます。
こういうやつ
一覧 | 地方フィルター | お天気 |
---|---|---|
M, V を別モジュールにしていましたが、ReactiveSwift と ReactiveCocoa を V と VM にインポートする必要があるのでメインに統合しました。
MVVM による分割
View
View がやるのは見た目に関すること、画面遷移系(これは VC が V である以上こいつがやるしかない)、ユーザーアクションの受け取りなど。
MVC の V と C の役割の プレゼンテーションロジックと M との仲介以外はこいつがやる。
- View の設定 (add したりとか)
- View のレイアウト設定
- ボタンとかのアクション実装 (VM に伝達)
- 画面遷移
ボタンとかのアクションの VM への通知に Action
とか使って頑張ったけど、もしかしたら IBAction 接続してそこで VM のメソッド呼び出しでもいいのかもしれない
Model
Model がやるのは UI 関係の処理以外すべて。MVC の M と同様。
- 天気の取得(通信まわり)
- お気に入りの保存
- 都道府県の情報取得
- どんなデータを受け取るかの表現([String: Any] で受けているところを Weather などの型で表現する)
- 都道府県のフィルター処理(これはもしかしたら P ロジックで VM がやるべきかもしれない)
- データが更新されたら VM に通知する
ViewModel
ViewModel がやるのはプレゼンテーションロジックと M と V の仲介。MVC の C がやってた一部をこいつがやる。
- 表示用のデータ加工(ex. 最高気温とかを加工して 30℃ とかにする)
- プレゼンテーション関連の状態保持(ex. ボタンのチェック状態の保持とか)
- V の表示更新(バインディング)
- M の更新を受け取る (コールバックとかデリゲート??)
とりあえずは上のように分離する。ここでもう一度 MVVM 導入の目的について考える。
- プレゼンテーションロジックのテストするため
- FatViewController を防止するため
2. は VM に分離したのである程度達成されたが、1.はまだ怪しい。VM のテストをしようと思うと M が必要になってくる。なのでこの部分を Protocol として切り出していく。
Protocol 切り出し
都道府県一覧画面の部分をこんな感じで切り出してみた。
PrefectureListViewModel プロトコル
protocol PrefectureListModelInput {
var prefectureList: [CityData] { get }
func updateFavoriteIds(cityId: String) -> Result<[String], FavoritePrefectureDataStoreError>
var favoriteIds: [String] { get }
func prefectureFiltered(isFilterFavorite: Bool,
favoriteIds: [String],
selectedAreaIds: Set<Int>) -> [CityData]
}
extension PrefectureListModel: PrefectureListModelInput {
}
import Models
は VM でのみやるべきかと思い、 extension PrefectureListModel: PrefectureListModelInput
のようにした。
これで VM のテストがしやすくなった。
上記のように分離していき下記が完成した。
テストはこんな感じ
テストを書くのが目的みたいに書いてたので無理やり色々テストを書いてみましたがこれであってるのかは不明。(MVP より下がってしまった)
Cocoa MVC と比べて V の再利用性は低下してしまったように思う。(M は独立したままなので再利用性は高い)。まあでも、下記の導入の目的は達成されたと思う。
- プレゼンテーションロジックのテストするため
- FatViewController を防止するため
さいごに
以上が現時点での私の MVVM の認識です。今後やっぱちがうなとか思ったら都度修正していきます
記事を書いたのは他の人の意見も聞きたいなと思ったからなので「その認識で合ってるよ」とか「全然ちゃうわ。頭わいてんのか?」など優しく指摘してくださると幸いです
とりあえずみんな iOSアプリ設計パターン入門 を読もう
iOS で MVVM をやろうと思うと ReactiveSwift, RxSwift など使うことになると思いますが、このライブラリのハードルが高い(まだ使い方よくわかんねぇ)ので実際のプロジェクトで導入する際はメンバーに熟練者がいるかよっぽどスケジュールに余裕がある場合以外安易に導入しない方がいいんじゃないかなと思いました。(やれること多過ぎ)