VIPERを勉強してみる(コード分析編)
モチベーション
iOSでMVCやMVVMなどは触ってきましたが、最近いろいろなところでVIPERなるものを利用しているとのこと。
恥ずかしながら触ったことがなく、前々から字面は見ていたのですがFlutterを主に触っていたのでスルーしていました。
ただMVCやMVVMに取って代わるぐらいに標準的なものになってきている感じがするので、どんなものかを確認しつつ理解していければと思っています。
VIPERとは
元ネタは以下らしい
(この書き込みから今のほぼ標準まで広がるのはすごいですね、、)View, Interactor, Presernter, Entity, RouterをあわせてVIPERという。
以下、画像を上記のブログから拝借します。
ざっと各役割をまとめてみます。
(処理の繋がりを意識したいので、View -> Presernter -> Interactor -> Entity -> Routerの順で書きます。)
View
ViewはUIView/UIViewController。
ViewControllerもViewにふくめ、インプット周りに関してはPresernterが管理する。
Presenter
Presenterはインプットを受け付けロジック側に処理をさせる。またはロジック層から返却された値をView側に伝える役割。
またRouterに対して遷移処理を促すこともPresenterで行われるそう。
UIKitには依存しないことを意識するとのこと。
Interactor
UseCaseをさばくうロジック層。
通信をしたり、保存をしたり、計算をしたりetc..
outputをprotocol定義をして、Delegate的な扱いでPresenterにて処理結果をうけとり、PresernterからViewに対して反映しています。
Entity
アプリ内で利用されるデータ定義。
ロジックは含まず、定義のみ。
Router
画面遷移のつなぎ、遷移処理、DIもここでやりそう。
ここがVIPER内で最も特徴なところらしく、今後くわしく見ていきます。
試しに触ってみる
上記の記事でコードも用意してくれているので、とりあえずはcloneしてみて、どんなふうにファイルやフォルダ分けをして責務分解をしているのかを見てみます。
cloneしたプロジェクトをXcode14.3.1で開きましたが、Swiftのバージョンアップとminumum ios targetとAlamofireを利用しているところを少し修正する必要がありました。
(7年前なので、それはそう。残していただけてるだけでありがたい、、)
またサンプル用に利用されているAPIが無効(404)になっているようだったので、起動してもなにも表示されませんでした。
作りをみるだけで、特に問題ないので見ていきます。
フォルダ階層
Aricles
|- Application
|- Resources
|- Enums
|- Extensions
|- Models
|- Modules
|- Network
|- Protocols
VIPERに関係ありそうなフォルダは「Modules」に格納されていたので、「Modules」内をより詳しく見ていきます。
Modules
|- Root
| |- RootContract.swift
| |- RootRouter.swift
|- Articles
| |- Contract
| |- Interactor
| |- Presenter
| |- Router
| |- View
|- Details
| |- Contract
| |- Presenter
| |- Router
| |- View
Root
まずはRootについて、
AppDelegateから呼び出しています。
アプリ起動後の最初の画面を制御していそうでした。
RootContract.swiftには、RootWireframeのprotocol定義がされており、presentArticlesScreenという関数宣言がされていました。
RootRouter.swiftはRootWireframeの実装が書かれていました。
window.makeKeyAndVisible()でWindowを最前面に設定し、
window.rootViewController = ArticlesRouter.assembleModule()で
ArticlesRouterのRouterを呼び出します。
Articles
Article+複数形なので、リストが表示される系の機能が実装されている感じと思われます。
また、アプリが起動したときにこの機能の画面を表示されます。
次に各フォルダの内部を見ていきます。
Contract
名前の通りですが、protocol定義のみがされており実装はしていませんでした。
ArticlesContract.swiftというファイルに、以下複数のprotocolが定義されていました。
- ArticlesView
- ArticlesPresentation
- ArticlesUseCase
- ArticlesInteractorOutput
- ArticlesWireframe
どうやらContractはそれぞれの階層で継承する用のフォルダみたいでした。
なので、UIからのアクションを増やす場合や引数を増やす系の修正についてはまずはContractを修正する必要がありそうです。
Interactor
ArticlesInteractor.swiftというファイルがあり、ArticleUseCaseを継承していました。
また、内部でArticlesInteractorOutputの宣言がありました。
class ArticlesInteractor: ArticlesUseCase {
weak var output: ArticlesInteractorOutput!
private var disposeBag = DisposeBag()
ロジック関連を管理するということで、UIKitのimportはありませんでした。
また、現状一つのInteractorに一つのUseCaseを継承していますが、複数のUseCaseがある場合は~~~Intaractorを増やすよりかは、このArticlesInteractorに同じように継承していく感じですかね?(おそらく)
もちろん現状ArticlesInteractorはArticleの一覧を取得し、取得に成功したときはArticles情報、失敗したときは失敗する通知を行うという機能を実装しているので、全く別の機能を実装する場合はもう一つInteractorを追加することになりそうです。
お気に入り機能やら、削除機能など一覧に影響を及ぼす機能に関してはArticlesInteractorに追加していく感じになりそうです。
また、Outputは1つのIntaractorに対して1つかもです。
Presenter
Presenter内部にはArticlesPresenter.swiftが格納されていました。
ArticlesPresentationを継承しており、内部にArticlesView,ArticlesUseCase,ArticlesWireframeの3つを持っていました。
class ArticlesPresenter: ArticlesPresentation {
weak var view: ArticlesView?
var interactor: ArticlesUseCase!
var router: ArticlesWireframe!
VIPERに関してロジックとViewを紐付ける重要なボトルネックになりそうな印象です。
このファイルにもUIKitのインポートはありません。
簡易的な文字のフォーマット変更やロジックから受け取ったデータによってUIを変更させる処理などはここに集約されそうです。
また、ArticlesPresenter
はArticlesInteractorOutput
も継承しており、ArticlesInteractor
で通信に成功したときによばれるarticlesFetched
と失敗したときに呼ばれるarticlesFetchFailed
の実装がされていました。
どうやらArticlesInteractor
内で定義されていたArticlesInteractorOutput
の実体はArticlesPresenter
のようでした。
View
こちらは見知ったStoryboard、ViewController、xibとそれに紐づくswiftファイルが格納されていました。
- ArticlesStoryboard.storyboard
- ArticlesViewController.swift
- ArticleTableViewCell.swift
- ArticleTableViewCell.xib
Cellに関しては、ViewControllerからしか呼ばれずセットした情報を表示するというミニマム実装だったのでスルー。
ArticlesViewControllerにはArticlesPresentasionが定義されており、viewDidLoadやセルがタップされたときなどにpresentationにイベントを送っているようでした。
また、ArticlesPresenter
で宣言されていた、ArticlesView
の実装はArticlesViewController
で実装されておりArticlesPresenter
内のArticlesView
の実体はArticlesViewController
でした。
Router
最後にRouterです。
ArticlesRouter.swift
のみが入っており、ArticlesRouter
というクラスで、ArticlesWireframeを継承していました。
また、ViewControllerを保持しています。
class ArticlesRouter: ArticlesWireframe {
weak var viewController: UIViewController?
private(set) var sortCompletion: ((ArticlesSortType) -> ())?
staticでassembleModule
という関数を定義しており、ここが起点のようです。
- ViewControllerの生成
- Presenterの生成
- Interactorの生成
- UINavigationControllerのrootViewControllerとして1をセット
- Router(自分自身) の生成
- 各階層に参照を繋ぐ(DI的な)
- 最後に4で生成したUINavigationControllerをリターンする
view?.presenter = presenter
presenter.view = view
presenter.interactor = interactor
presenter.router = router
interactor.output = presenter
router.viewController = view
ここでpresenterとinteractorにそれぞれinteractor,presenterを渡していて循環参照ぽくないか?と思いつつ、そのために宣言時にweakをつけているんですね。
他に、preserntSortOptions
という関数があり、AlrtDialogを表示する処理とpresentDetailsと詳細画面に遷移する処理がありました。
なので、Dialog含め遷移に関わる実装というのはrouterで制御されそうでした。
例えば、「編集中」「内容を破棄しますか?」みたいなダイアログの表示など。
Details
Contract,Presenter,Viewの流れはArticlesと一緒なのでRouterのみ詳しく見てみようと思います。
と、言いつつassembleModule
の関数があるのもArticlesRouterと同じで処理の流れもほぼほぼ同じでした。
一点違うのは、UINavigationControllerではなく単純なStoryBoard経由で取得したViewControllerをリターンしていました。
なので、UINavigationControllerをリターンするのは最初の画面だけで他の画面は不要そうでした。
まとめ
大本になった記事とコードを見ることである程度理解できたと思います。
これらをもとにやってみたいこととしては、
- SwiftUIになった場合
- 画面をリアルタイムで変更したい場合(今回はListViewだったのでリロードするだけでした)
- より画面遷移が複雑になる場合
ここらへんを実際に書きながら、より整理していこうと思います。
Discussion