🚀

VIPERを勉強してみる(コード分析編)

2023/09/18に公開

モチベーション

iOSでMVCやMVVMなどは触ってきましたが、最近いろいろなところでVIPERなるものを利用しているとのこと。
恥ずかしながら触ったことがなく、前々から字面は見ていたのですがFlutterを主に触っていたのでスルーしていました。
ただMVCやMVVMに取って代わるぐらいに標準的なものになってきている感じがするので、どんなものかを確認しつつ理解していければと思っています。

VIPERとは

元ネタは以下らしい
https://cheesecakelabs.com/blog/ios-project-architecture-using-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してみて、どんなふうにファイルやフォルダ分けをして責務分解をしているのかを見てみます。
https://github.com/pedrohperalta/Articles-iOS-VIPER

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の宣言がありました。

ArticlesInteractor.swift
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つを持っていました。

ArticlesPresenter.swift
class ArticlesPresenter: ArticlesPresentation {
    
    weak var view: ArticlesView?
    var interactor: ArticlesUseCase!
    var router: ArticlesWireframe!

VIPERに関してロジックとViewを紐付ける重要なボトルネックになりそうな印象です。
このファイルにもUIKitのインポートはありません。

簡易的な文字のフォーマット変更やロジックから受け取ったデータによってUIを変更させる処理などはここに集約されそうです。

また、ArticlesPresenterArticlesInteractorOutputも継承しており、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を保持しています。

ArticlesRouter.swift
class ArticlesRouter: ArticlesWireframe {
    
    weak var viewController: UIViewController?
    private(set) var sortCompletion: ((ArticlesSortType) -> ())?

staticでassembleModuleという関数を定義しており、ここが起点のようです。

  1. ViewControllerの生成
  2. Presenterの生成
  3. Interactorの生成
  4. UINavigationControllerのrootViewControllerとして1をセット
  5. Router(自分自身) の生成
  6. 各階層に参照を繋ぐ(DI的な)
  7. 最後に4で生成したUINavigationControllerをリターンする
ArticlesRouter.swift
        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