注:これはポエムです。
タイトルのままです。
最近諸事情により新しくiOSアプリを作っています。 めちゃくちゃ複雑とか、エキセントリックなアプリというわけではなく、極めて普通のアプリです。
コンプライアンス的なアレがアレなのでアレですが、Twitter公式アプリのような内容感です(?)。
今回そのアプリの開発にVIPERを採用した訳ですが、なぜそれを採用するに至ったかの経緯と理由のポエムを綴ります。
参考にした記事等は以下です
GitHub - pedrohperalta/Articles-iOS-VIPER: Demo app for VIPER design pattern implementation
iOS Project Architecture : Using VIPER [和訳] - Qiita
MVCってなに
MVCがわからない
恥ずかしながら、僕はこれまで特に何も考えず(何も考えていない訳ではないが)に、Model-View-Controller: MVCで開発をしていました。(本当にそれがMVCなのか?と言われると胸を張ってコレがMVCだ!とは言えないが)
ですが、iOSアプリ開発でMVCとはどういうものでしょうか。ViewはUIViewの事、ControllerはUIViewControllerの事でしょう。では、Modelとはナンですか、ViewとController以外のものっていっぱいあるし、それを全部Modelというのでしょうか。ヨクワカンナイ。 そもそもViewControllerって何、Viewを制御するものだしコレこそがViewでは?
MVCとは何かということに関しては、ググればたくさんWeb系の記事とかが出てきますが、Webとスマホアプリって違う事が多いし、何よりデータ取得まわりが全然違うから、同じように考えることって厳しいのではないでしょうか。
例として、Twitterアプリを作るとしましょう。 APIアクセスをしてツイート一覧を取得し、ツイート一覧画面と、ツイートをタップした時に詳細画面、ユーザーアイコンをタップした時にユーザー詳細画面を表示する事を考えます。
Viewは置いておいて、class, structはこんな感じでしょうか
- Tweet一覧VC
- Tweet詳細VC
- User詳細VC
- TweetModel
- UserModel
- APIService
TweetやUserといったModelはなんかRails等のModelと似たようなものを感じます ここでよくわからないのが、APIServiceはModelなのか、という事です。 Railsのように、Modelとは所謂Entityのようなもので、JSONの結果をMappingしたものであると考えると、APIServiceというのはなんかおかしいような感じがします。
では、なんか同列にするのは気持ち悪いので、APIServiceはModelではなく、Serviceという括りにしてしまいましょう。MVCSとでも呼びましょうか。
MVCSなら大丈夫?
[ View ] - [Controller] - [Service] - [Model]
APIServiceはControllerから呼ばれ、Callback等でObjectMapper等でJSONのデータがMappingされたTweetのListやUserなどが返ってくる、とするといい感じな気がします。
こんな感じで、APIアクセスや内部のデータ保存等は全部Serviceに任せてしまえ、というのがこれまで僕がよくやってたやり方ですが、このやり方だと段々つらくなってきました。
MVCSでもやっぱりつらい
つらいポイントは2つあります。
1つめは、CSMの間で起きる問題です。例えばツイートの一覧を取得する際にミュートしているユーザーのツイートは除去するという処理を挟むとします。APIServiceから返ってくるのは全てのツイートだとすると、ツイートの除去という処理はController、ここではViewControllerで行う必要が出てきます。mapで1行で書ける処理ではありますが、なんやらかんやらしてると一瞬でControllerが太りかねません。これはServiceの導入によっても生じてしまう問題です。
もう1つは、switch文で書くような遷移やユーザーの動作に伴う処理がたくさんあるような画面、例えば"設定"のアプリの画面とかだとViewControllerがまたまたブクブクと太っていきます。これはService関係無く、MVCというもの自体に限界があるような気がします。
そもそもMVCはつらい
1つめの問題は置いておいて、2つめのユーザーの動作に関連する処理によってViewControllerが太る問題の原因と対策を考えましょう。
例として、Twitter公式アプリのこの画面を実装する事を考えます。
StoryboardでtableViewのstatic cellに全部をポチポチ入力していくというのはナシで、tableViewにcustomCellを突っ込んでいくような感じでいきましょう。
ここではプライバシー等に関する設定をAPIから取得して、それを表示し、変更があったらAPIに変更を投げるという処理が必要になります。 CustomCellを作ることにはなると思いますが、それらのCellからAPIServiceを呼び出してAPIアクセスをするというのはナシにしましょう。 あくまでAPIアクセスをするのはControllerとしましょう。 ですが、恐ろしい事に"データ取得", "データ表示", "データ更新"の全てをこのViewControllerに書かないといけません。
PrivacySettingというモデルがあり、viewDidLoad
でAPI経由でこれを取得して、tableView.reloadData()
されたとします。
cellの描写を行う、func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
ではswitchないし配列でそれぞれのsectionとrowのCustomCellを選択し、それぞれの説明文とPrivacySettingの中の設定値をcellにセットします。
これは本来のViewの役割という感じがしますね。
一部のcellはタップされたら遷移が必要なので、func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
で分岐処理を少し書く必要がありますね。この画面では必要無いですが、遷移先のVCに遷移元のVCの情報を渡す等の処理が必要であれば、case文の中に書くorメソッドを分ける必要がありそうです。太りますね〜
cellの中にUISwitchControlが入っているものは、どうにかしてVCまで変更を通知させましょう。cellを生成する時にclosureを渡すとか、参照を渡すとかetcでなんとかしました。
Protect your tweetsのUISwitchControlが変更されたとして、PrivacySettingViewController#updateProtectTweetSetting(protect: Bool)
が呼ばれたとしましょう。ここではAPIServiceを通じてツイートの保護の設定を変更する必要があります。当然エラー処理等も必要です。
複雑な画面になると、遷移であったりAPI関連の処理だったりも複雑になります。これらの実処理はControllerがやることなので正しいことではあるんですが、Viewからの通知を直接VCが受けて、処理をしていますね。
また、複雑な画面なのでcellの描写が複雑になるのは仕方ないでしょう。というかこれってViewの処理ですよね……?あれ……というかこれもMVじゃねーか!
なんか諸悪の根源はViewControllerだという気がしてきました。ViewControllerで重たいViewの描写をして、Controllerがやるべき処理もここでやってるのが悪いんではないでしょうか。
では、ViewControllerにControllerは荷が重いので、VCにはViewの描写に専念してもらって、タップ等はViewからControllerに通知して、実処理を行ってもらうようにしましょう。
ここで、最優秀選手ではなくModel-View-Presenter: MVPの登場です。
MVP
MVPは最優秀選手なのか
iOSアプリ開発でMVCは厳しい、ViewControllerにControllerは荷が重すぎる、という話でした。
MVPを採用して、UIViewとUIViewControllerをViewとして扱うようにしましょう。
先程のPrivacy and safetyの画面を考えましょう。 項目がたくさんあるのでViewはそれなりに複雑になりますが、API経由の取得や変更処理はPresenterに書き、ViewControllerからそれを呼ぶという感じにすれば、かなりスッキリしそうです。
また、テストもかなりやりやすくなりますね。 ツイート一覧を表示する画面、HomeViewのテストをするとすると、
protocol HomeView: class { var presenter: HomePresenter! { get set } func showTweets(_ tweets: [Tweets]) } protocol HomePresenter: class { weak var view: HomeView? func loadTweets() } class HomePresenterImpl: HomePresenter { weak var view: HomeView? func loadTweets() { APIService.fetchTweets { self.view.showTweets($0) } } } // Test class HomePresenterMock: HomePresenter { weak var view: HomeView? func loadTweets() { self.view.showTweets([Tweet(...), Tweet(...)]) } }
こんな感じでしょうか。いい感じですね〜これで採用でいきましょうか!
……と思ったら先程のもう一つの問題、Modelがナニカヨクワカラナイ、Serviceを導入してもModelの取得に関連してControllerが太る問題がありました。
Modelの曖昧性を無くそう
MVPはそのままで、Modelの曖昧性を無くす努力をしましょう。
先程Serviceを導入した際、Modelとは所謂Entityのようなもので、みたいな事を言いました。 Modelとは何かよくわからないので、データの入れ物、データを表現するものとしてEntityということにしましょう。
では、このEntity等をどうやって取得しましょう。APIServiceを使ってPresenterから呼びましょうか?
実際のところ、APIの変更や再利用性を考えると、複数のControllerもといPresenterから呼び出されるAPIServiceは抽象化されているのが望ましいですが、抽象化されているということはPresenterが肥大しかねません。また、データの永続化が絡んでくると更にPresenterが太りそうです。
どういう事かというと、仮にツイートを取得する/tweets
があったとして、?is_mute=ture
でミュートしているユーザーのツイートを取得するかどうかを切り替えられるとしましょう。
その時、APIService.fetchTweet()
とAPIService.fetchTweet()
の2種類のメソッド(内部的に同一のメソッドを呼ぶというのもあるけど)でそれぞれ通信処理を書くより、APIService.fetchTweet(isMute: Bool)
としたほうが/tweets
の仕様変更にも追従しやすいですよね。
また、1つのWebサービスで複数のiOSアプリを出すとすると、APIアクセスをする部分は共通のライブラリ化したほうが良いですよね。
そう考えるとAPIServiceのような汎用的なAPIアクセスを提供するものと、Presenterが欲しいものはどんどん離れていきます。 将来的な変更等も想定してゼロからアプリを組むなら、少しコード量が増えるのには目を瞑ってPresenterが欲しいデータを取得する専門の何か、があると良いのでは無いでしょうか?
どんどんVIPERに近づいて来ましたが、ここでInteractorの登場です。
VIPE
MVPから、ModelをEntityとして、Interactorを導入しました。View-Interactor-Presenter-Entity: VIPE 的な何かが誕生しました。
先程のServiceのようなものはライブラリ化するにしても、Interactorに内包されたとみなします。
これで過剰に肥大化する箇所は特に無く、完璧なような気がします!!!
それぞれの役割を確認しましょう、
- View: びゅー
- Presenter: UIの操作を行う、必要なデータはInteractorから取得する
- Interactor: Entityをどうにかこうにか用意する
- Entity: データ
残る問題としては、遷移をどこに任せるか微妙という事でしょうか。 この中だとPresenterに遷移を任せるのが良いような気がしますが、遷移が複雑な画面の場合Presenterが複雑になりそうですね。それにPresenterの役割は、Viewに必要なデータをInteractorから取ってくる、というもののはず。なんか遷移はこれとは違うような?
余談ですが、面白い記事を見つけました tech.mercari.com
よし、遷移を管理するRouterを用意しよう!
[View] - [Interactor] - [Presenter] - [Entity] - [Router]
VIPERになりました
VIPERで本当に良いのか
ここまで来ましたが、VIPERで本当に良いのでしょうか?
まぁそれはわかりません。だってまだぜんぜん開発してないもん。
ここまでは僕の頭の中での思考の結果です。実際は作ってみないとわかりません!!!!
実装にあたっての構造と命名について
実際にVIPERを採用して実装するにあたって、protocolの命名やファイルの構造についてちょっと悩みましたが結局以下のような感じに落ち着きました。
まず、それぞれの層は他の層について最低限の事を知り合って、"契約"を結ぶ必要があります。これはprotocolで実現されます。
つまり、ViewはPresenterを持ち(強参照し)、PresenterはViewにデータを渡す必要があります(弱参照で実現できる)
また、PresenterはInteractorの為に、Interactorからの出力を受け付ける口を持つ必要がありますね。
これらの契約関係をまとめてContract
に記述しています。
例えばこんな感じに、
// HomeContract.swift protocol HomeView: class { var presenter: HomePresentation! { get set } func showTweets(tweets: [Tweet]) } protocol HomePresentation: class { weak var view: HomeView? { get set } var interactor: HomeInteraction! { get set } var router: HomeRoute! { get set } func didSelectTweet(tweet: Tweet) } protocol HomeInteraction: class { weak var output: HomeInteractorOutput! { get set } func fetchTweets() } protocol HomeInteractorOutput: class { func tweetsFetched(tweets: [Tweet]) func tweetsFetchFailed() } protocol HomeRoute: class { weak var viewController: UIViewController? { get set } static func assembleModule() -> UIViewController func presentDetails(tweet: Tweet) }
protocolの実装は
HomeViewController: HomeView
HomePresenter: HomePresentation, HomeInteractorOutput
HomeInteractor: HomeInteraction
HomeRouter: HomeRoute
という感じです。 人によっていろんな名前にしてるっぽくて、これが正しいのかはわかりませんが、こんな感じでしています。
なんども言いますが
これはポエムです。
なんか間違ってる事言って叩かれるとガラスのハートが壊れてしまうので叩かないで欲しいのですが、詳しい人のコメントとか頂けるととてもうれしいです。
事の発端は、RailsとかでWebを書くマンと話をしていたときのModelに対する考え方の相違です。
RailsのModelというものが正しいのかどうかはよくわかりませんが、個人で小規模のRailsアプリを書いている時でもModel=DBのTableのような考えでも困らないことが多いのに、アプリになるとModel=APIのCRUD群みたいな考えだと途端に辛くなります。
というのも、APIの取得処理であったりLocal Storageへの保存の処理がアプリだと非同期になったり、コード自体長くなるから、だと思います。(それをカプセル化したものがMVSCで、なんとかしていた)
MVCとかはよくWebの土俵で議論される事が多いですが、アプリでもアーキテクチャは重要でかつ色々な問題を抱えているので、今後も勉強して良いアプリを作りたい所存。
(しかし小規模アプリではちょっとダルいみたいなところあるな)
【追記】2018年1月19日
XcodeのTemplateを作った、ちょっとかなり楽になった