VIPERアーキテクチャ まとめ

# VIPERアーキテクチャとは?
![VIPER_img](https://s3.amazonaws.com/ckl-website-static/wp-content/uploads/2016/04/viper_architecture-1280x680.jpg)

* 公式 : [iOS Project Architecture: Using VIPER](https://cheesecakelabs.com/blog/ios-project-architecture-using-viper/)
    * [翻訳した記事](https://qiita.com/YKEI_mrn/items/67735d8ebc9a83fffd29)
* iOSの開発現場で生まれたアーキテクチャである
* `View` , `Interactor` , `Presenter` , `Entity` , `Router` の頭文字を取ってVIPERと呼ぶ
    * ※それぞれの詳しい役割やサンプルコードは後述

# VIPERアーキテクチャの理念
* [単一責任の原則](https://drive.google.com/file/d/0ByOwmqah_nuGNHEtcU5OekdDMkk/view)のもと適切に分割すること
    * ∴「このクラスは〇〇をするクラスである」と一言で簡単に説明ができる
* `View` , `Interactor` , `Presenter` , `Router` はそれぞれprotocolを切り、それに準拠した実装を行う
    * 実際にそれぞれを組み合わせる際は、protocolを介してアクセスすること  
      →**protocolにのみ依存している**状態にすること
    * これによりそれぞれの要素の差し替えが容易になり、テストコードが書けるようになる

# VIPERそれぞれの役割
## 関係
![relationship](https://s3.amazonaws.com/ckl-website-static/wp-content/uploads/2016/04/Viper-Module.png)

### ざっくりと処理の流れ

1. `Router` で画面を生成し、必要な依存性を注入(以下DI)させる
2. 1.で生成された画面を表示
3. `View` からイベントを `Presenter` にお知らせする
    * ライフサイクル
    * ユーザのタップ
    * …などなど
4. `Presenter` は `View` から送られてきたイベントの内容に合わせて以下のような処理を実行する
    * `View` に対して画面の更新依頼を投げる
        * `View` は依頼された通りに画面の更新をする
    * `Interactor` に対してデータの取得依頼を投げる
        * `Interactor` は依頼されたデータ取得が完了したら `Presenter` に通知する
    * `Router` に対して画面遷移の依頼を投げる
        * `Router` は依頼された画面へ遷移する → 1. へ戻る

## 命名
| 役割                 | プロトコル名                     | 実体名                           |
|:---------------------|:---------------------------------|:---------------------------------|
| `View`               | `{ModuleName}View`               | `{ModuleName}ViewController`     |
| `Presenter`          | `{ModuleName}Presentation`       | `{ModuleName}Presenter`          |
| `Router`             | `{ModuleName}Wireframe`          | `{ModuleName}Router`             |
| `Interactor`         | `{UsecaseName}Usecase` ※1       | `{UsecaseName}Interactor` ※1    |
| `Entity`             | - ※2                            | `{~~}Entity` ※3                 |

※1:Interactorは利用シーン(ユースケース)に応じてクラス分割するため、画面と1:1ではない
※2:Entityはテストの際に差し替える必要がないほどシンプルな構造なのでプロトコル不要
※3:1つのモジュールで複数のEntityを扱うこともあるため

## それぞれの役割の詳細
以下では[サンプルコード](https://github.com/hicka04/viper-sample)を元に説明
GitHubのAPIをたたいてリポジトリの一覧を取得し表示する

### `Router`
* 「画面遷移」と「DI」を担当
* VIPERアーキテクチャの肝であり、他の有名アーキテクチャにないところ
    * 他アーキテクチャでは`View`に画面遷移の処理もお願いする必要があり、`View`が「画面の更新」と「画面遷移」の2つを担当する必要があった  
      →`View`のコードの見通しが悪くなりがちだった
    * VIPERでは「画面遷移」の処理を `Router` に移管したことで `View` の責務を減らせ、可読性の向上が望める

[`Router` のサンプル](https://github.com/hicka04/viper-sample/blob/master/ViperSample/Modules/RepositorySearchResult/Router/RepositorySearchResultRouter.swift)

```Swift:RepositorySearchResultRouter.swift
import UIKit

protocol RepositorySearchResultWireframe: AnyObject {
    func showRepositoryDetail(_ repository: RepositoryEntity)
}

final class RepositorySearchResultRouter {
    // 画面遷移のためにViewControllerが必要。initで受け取る
    private unowned let viewController: UIViewController

    private init(viewController: UIViewController) {
        self.viewController = viewController
    }

    // DI
    static func assembleModules() -> UIViewController {
        let view = RepositorySearchResultViewController()
        let router = RepositorySearchResultRouter(viewController: view)
        let searchRepositoryInteractor = SearchRepositoryInteractor()
        // PresenterはView, Interactor, Routerそれぞれ必要なので
        // 生成し、initの引数で渡す
        let presenter = RepositorySearchResultPresenter(
            view: view,
            router: router,
            searchRepositoryInteractor: searchRepositoryInteractor
        )

        view.presenter = presenter    // ViewにPresenterを設定
        return view
    }
}

// Routerのプロトコルに準拠する
// 遷移する各画面ごとにメソッドを定義
extension RepositorySearchResultRouter: RepositorySearchResultWireframe {
    func showRepositoryDetail(_ repository: RepositoryEntity) {
        // 詳細画面のRouterに依存関係の解決を依頼
        let detailView = RepositoryDetailRouter.assembleModules(repository: repository)
        // 詳細画面に遷移
        // ここで、init時に受け取ったViewControllerを使う
        viewController.navigationController?.pushViewController(detailView, animated: true)
    }
}
```

### `View`
* 「画面の更新」と「 `Presenter` へのイベント通知」を担当
* 「画面の更新」
    * ラベルの文言変更
    * `UITableView` のリロード
    * …などなど
* 「 `Presenter` へのイベント通知」
    * ライフサイクル( `viewDidLoad()` , `viewWillAppear()` …など)
    * ボタンのタップ、セルのタップなど
* `UIView` , `UIViewController` が該当
    * 1ViewControllerに対して1protocolを切る

[`View`のサンプル](https://github.com/hicka04/viper-sample/blob/master/ViperSample/Modules/RepositorySearchResult/View/RepositorySearchResultViewController.swift)

```Swift:RepositorySearchResultViewController.swift
import UIKit

protocol RepositorySearchResultView: AnyObject {
    func updateRepositories(_ repositories: [RepositoryEntity])
    func showErrorAlert()
}

class RepositorySearchResultViewController: UIViewController {
    // Presenterへのアクセスはprotocolを介して行う
    var presenter: RepositoryListViewPresentation!
    
    private var repositories: [RepositoryEntity] = [] {
        didSet {
            DispatchQueue.main.async {
                self.tableView.reloadData() // 画面の更新
            }
        }
    }
}

// Viewのプロトコルに準拠する
extension RepositoryListViewController: RepositoryListView {
    func updateRepositories(_ repositories: [RepositoryEntity]) {
        self.repositories = repositories
    }
}

extension RepositorySearchResultViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        guard let text = searchBar.text else { return }
        
        // Presenterにイベント通知
        presenter.searchButtonDidPush(searchText: text)
        
        searchBar.resignFirstResponder()
    }
}
```

### `Presenter`
* `View` から受け取ったイベントを元に別のクラスに依頼する
    * `View` に対して画面の更新を依頼する
    * `Interactor` に対してデータの取得を依頼する
    * `Router` に対して画面遷移を依頼する
* `Presenter` が提供するメソッド名は  
  「画面の更新が終わった( `viewDidLoad` )」「ボタンが押された( `hogeButtonDidPush` )」  
  といった命名にすること
    * ×「(ボタンが押されたので)詳細画面に遷移する(`showDetailView`)」
* `Presenter` には状態を持たせない (ハブに徹して、依頼する)
    * 画面表示に必要な状態は `View` に持たせるなど、適切な場所で状態管理をすべき
    * `Presenter` に状態を持たせると `Presenter` のテストコードが書きづらくなる
* **`import UIKit` 禁止**
    * UIがどうなっているかを気にしない

[`Presenter` のサンプル](https://github.com/hicka04/viper-sample/blob/master/ViperSample/Modules/RepositorySearchResult/Presenter/RepositorySearchResultPresenter.swift)

```Swift:RepositorySearchResultPresenter.swift
import Foundation

protocol RepositorySearchResultPresentation: AnyObject {
    func searchButtonDidPush(searchText: String)
    func didSelect(repository: RepositoryEntity)
}

class RepositorySearchResultPresenter {
    // View, Interactor, Routerへのアクセスはprotocolを介して行う
    // Viewは循環参照にならないよう`weak`プロパティ
    private weak var view: RepositoryListView?
    private let router: RepositoryListWireframe
    private let searchRepositoryInteractor: SearchRepositoryUsecase

    init(view: RepositorySearchResultView,
         router: RepositorySearchResultWireframe,
         searchRepositoryInteractor: SearchRepositoryUsecase) {
        self.view = view
        self.router = router
        self.searchRepositoryInteractor = searchRepositoryInteractor
    }
}

// Presenterのプロトコルに準拠する
extension RepositorySearchResultPresenter: RepositorySearchResultPresentation {
    func searchButtonDidPush(searchText: String) {
        guard !searchText.isEmpty else { return }
        // Interactorにデータ取得処理を依頼
        // `@escaping`がついているクロージャの場合は循環参照にならないよう`[weak self]`でキャプチャ
        searchRepositoryInteractor.fetchRepositories(keyword: searchText) { [weak self] result in
            switch result {
            case .success(let repositories):
                self?.view?.updateRepositories(repositories)
            case .failure:
                self?.view?.showErrorAlert()
            }
        }
    }
    
    func didSelect(repository: RepositoryEntity) {
        router.showRepositoryDetail(repository)
    }
}
```

### `Interactor`
* 「ビジネスロジック」を担当
* `Presenter` から依頼されたビジネスロジックを実施し、結果を返す
    * 非同期で結果を返すのがおすすめ (UnitテストのためのMock作成が楽になる)
        * クロージャ
        * Delegate
        * Combine, RxSwiftなどのリアクティブプログラミングフレームワーク
    * 循環参照にならないよう実装注意
* **`import UIKit` 禁止**
    * UIがどうなっているかを気にしない

[`Interactor` のサンプル](https://github.com/hicka04/viper-sample/blob/master/ViperSample/Domain/SearchRepository/SearchRepositoryInteractor.swift)

```Swift:SearchRepositoryInteractor.swift
import Foundation

protocol SearchRepositoryUsecase: AnyObject {
    func fetchRepositories(keyword: String,
                           completion: @escaping (Result<[RepositoryEntity], Error>) -> Void)
}

final class SearchRepositoryInteractor {
    // GitHubに問い合わせるためのAPIクライアント
    // Interactorのテスト時にAPIクライアントをMockに差し替えて任意のレスポンスを返すようにするため
    private let client: GitHubRequestable
    
    init(client: GitHubRequestable = GitHubClient()) {
        self.client = client
    }
}

// Interactorのプロトコルに準拠する
extension SearchRepositoryInteractor: SearchRepositoryUsecase {
    func fetchRepositories(keyword: String,
                           completion: @escaping (Result<[RepositoryEntity], Error>) -> Void) {
        let request = GitHubAPI.SearchRepositories(keyword: keyword)
        client.send(request: request) { result in
            completion(result.map { $0.items })
        }
    }
}
```

### `Entity`
* 「データ構造の定義」を担当
* structでデータ構造を定義する
* **`import UIKit` 禁止**
    * UIがどうなっているかを気にしない
* 基本的にプロパティとイニシャライザのみ定義し、ロジックを持たないようにする

[`Entity` のサンプル](https://github.com/hicka04/viper-sample/blob/master/ViperSample/Infra/GitHubClient/Enitities/RepositoryEntity.swift)

```Swift:RepositoryEntity.swift
import Foundation

struct RepositoryEntity: Decodable {
    let id: Int
    let name: String
    let fullName: String
    let htmlURL: URL
    let starCount: Int
    let owner: UserEntity
    
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case fullName = "full_name"
        case htmlURL = "html_url"
        case starCount = "stargazers_count"
        case owner
    }
}
```

## ディレクトリ構造
```
├── Application
│   ├── Base.lproj
│   │   └── LaunchScreen.storyboard
│   ├── Delegate
│   │   ├── AppDelegate.swift
│   │   ├── Presenter
│   │   │   └── AppPresenter.swift
│   │   └── Router
│   │       └── AppRouter.swift
│   └── Info.plist
│
├── Modules
│   ├── RepositoryDetail
│   │   ├── Presenter
│   │   │   └── RepositoryDetailViewPresenter.swift
│   │   ├── Router
│   │   │   └── RepositoryDetailRouter.swift
│   │   └── View
│   │       ├── RepositoryDetailViewController.swift
│   │       └── RepositoryDetailViewController.xib
│   └── RepositorySearchResult
│       ├── Presenter
│       │   ├── RepositorySearchResultPresenter.swift
│       │   └── RepositorySearchResultPresenterTests.swift
│       ├── Router
│       │   └── RepositorySearchResultRouter.swift
│       └── View
│           ├── RepositorySearchResultViewController.swift
│           └── SubView
│               ├── RepositoryCell.swift
│               └── RepositoryCell.xib
│
├── Domain
│   └── SearchRepository
│       └── SearchRepositoryInteractor.swift
│
├── Infra
│   └── GitHubClient
│       ├── Enitities
│       │   ├── RepositoryEntity.swift
│       │   ├── SearchResponseEntity.swift
│       │   └── UserEntity.swift
│       ├── GitHubAPI.swift
│       └── GitHubClient.swift
│
├── Extensions
│   ├── ...
│   ├── ...
│
└── Resources
    └── Assets.xcassets
        ├── ...
```

# Unitテスト
* 流れ
    * テストしたい対象が依存しているクラスなどのMockを作成
    * ↑で作ったクラスなどを使ってテストしたい対象を初期化
    * テストしたい対象のメソッドを呼び出すことで起こる挙動を確認
* 注意点
    * Mockを自作することで、Mock自体にバグが発生する可能性がある  
    * protocolに1つメソッドを追加するだけでもテストコードを直す必要があり、大変
    * 解決方法
        * Mockライブラリ
            * [Cuckoo](https://github.com/Brightify/Cuckoo)
            * [SwiftyMocky](https://github.com/MakeAWishFoundation/SwiftyMocky)
        * Mock生成ツール
            * [Mockolo](https://github.com/uber/mockolo)
            * [SwiftMockGeneratorForXcode](https://github.com/seanhenry/SwiftMockGeneratorForXcode)

[`Presenter` のUnitテストのサンプル](https://github.com/hicka04/viper-sample/blob/master/ViperSample/Modules/RepositorySearchResult/Presenter/RepositorySearchResultPresenterTests.swift)

```Swift:RepositoryListPresenterTest.swift
import XCTest
// `@testable import`をするとinternalなクラスやプロトコルにアクセスできるようになる
@testable import ViperSample

class RepositorySearchResultPresenterTests: XCTestCase {
    var view: ViewMock!
    var router: RouterMock!
    var searchRepositoryInteractor: RepositoryInteractorMock!
    var presenter: RepositorySearchResultPresenter!

    override func setUp() {
        super.setUp()
        
        view = .init()
        router = .init()
        searchRepositoryInteractor = .init()
        presenter = .init(
            view: view,
            router: router,
            searchRepositoryInteractor: searchRepositoryInteractor
        )
    }
    
    func test_searchButtonDidPush() {
        // PresenterにsearchButtonDidPushイベントが届いたときの挙動をテスト
        // SearchRepositoryInteractorに検索を依頼しているか
        // 正常時やエラー時に適切にViewに描画依頼をしているか
        XCTContext.runActivity(named: "searchButtonDidPush") { _ in
            XCTContext.runActivity(named: "when before called") { _ in
                XCTContext.runActivity(named: "`fetchRepositories` is not called") { _ in
                    XCTAssertEqual(searchRepositoryInteractor.callCount_fetchRepositories, 0)
                }
            }
            
            XCTContext.runActivity(named: "when after called") { _ in
                XCTContext.runActivity(named: "`fetchRepositories` is called") { _ in
                    presenter.searchButtonDidPush(searchText: "Swift")
                    XCTAssertEqual(searchRepositoryInteractor.callCount_fetchRepositories, 1)
                }
                
                XCTContext.runActivity(named: "when `fetchRepositories` response error") { _ in
                    setUp()
                    searchRepositoryInteractor = .init(result: .failure(NSError()))
                    presenter = .init(view: view,
                                      router: router,
                                      searchRepositoryInteractor: searchRepositoryInteractor)
                    
                    presenter.searchButtonDidPush(searchText: "Swift")
                    
                    XCTContext.runActivity(named: "`showErrorMessageView` is called") { _ in
                        XCTAssertEqual(view.callCount_showErrorAlert, 1)
                    }
                }
                
                XCTContext.runActivity(named: "when `fetchRepositories` response succeed") { _ in
                    setUp()
                    let repositories = [
                        RepositoryEntity(id: 0,
                                   name: "Swift",
                                   fullName: "apple/Swift",
                                   htmlURL: URL(string: "https://github.com/apple/Swift/")!,
                                   starCount: 100000,
                                   owner: UserEntity(id: 0, login: "apple"))
                    ]
                    searchRepositoryInteractor = .init(result: .success(repositories))
                    presenter = .init(view: view,
                                      router: router,
                                      searchRepositoryInteractor: searchRepositoryInteractor)
                    
                    presenter.searchButtonDidPush(searchText: "Swift")
                    
                    XCTContext.runActivity(named: "`updateRepositories` is called") { _ in
                        XCTAssertEqual(view.callCount_updateRepositories, 1)
                    }
                }
            }
        }
    }
```

自作Mockの例

```Swift:Viewのモック
// Viewのprotocolに準拠
class ViewMock: RepositorySearchResultView {
    var callCount_updateRepositories = 0
    func updateRepositories(_ repositories: [Repository]) {
        // 呼ばれた回数を記録
        callCount_updateRepositories += 1
    }
}
```

# ファイルの自動生成
ここまでVIPERアーキテクチャにおけるそれぞれの役割や実装方法、テストの方法などを説明してきましたが、
**「必要なファイルが多い!」**と感じた方もいらっしゃると思います。
そこで以下に紹介するようなファイル自動生成ツールを使うことをおすすめします
ボイラープレートを自動生成できることで、実装者による差分をなくしたり、タイポをなくしたりと、工数削減に役立ちます

## [Generamba](https://github.com/strongself/Generamba)
* VIPER用に作られたファイル自動生成ツール
* インストールの手順に従うだけでVIPERのテンプレートもついてくる
    * が、デフォルトのテンプレートが使いづらかったため、自分用の[VIPERテンプレート](https://github.com/hicka04/generamba-templates)作成
    * [liquid](https://github.com/Shopify/liquid)という形式でテンプレートを記述


## [Kuri](https://github.com/bannzai/Kuri)
個人的おすすめ

* @bannzai さんが作成したファイル自動生成ツール
* もともとCreanArchitecture用に作られたツール
    * テンプレートを工夫すればVIPERでも利用可能
    * [VIPER用のテンプレート](https://github.com/hicka04/kuri-viper-template)作ってみました
* ymlの設定ファイルやコマンド実行時のオプションを活用することで、生成方法を細かく設定できる  
  Generambaでは実現できない以下のようなことができる
    * 生成したいコンポーネントを選択できる
        * Interactorは画面と1:1ではないので、以下の要件に柔軟に対応できる
            * View, Presenter, Routerだけを生成したい
            * Interactorだけを生成したい
    * コンポーネントごとに生成先のframeworkを選択できる
        * EmbeddedFrameworkを活用しているプロジェクトでも利用可能
* コマンドの詳しい使い方は以下を参考
    * https://github.com/bannzai/Kuri/blob/master/Documents/generate.md
    * https://dev.classmethod.jp/smartphone/iphone/introduction-of-kuri-generate-command-option/

# Combineを使った実装方法について
以下記事にまとめました
https://zenn.dev/hicka04/articles/viper-combine

# まとめ
* 単一責任の原則をもとに適切にクラス分割すること
* 画面生成は `Router` で行い、クラス内部で `init()` 等をしないこと
* 分割されたそれぞれのクラスはprotocolを介してアクセスすること
    * Unitテストの際に差し替えできるようにするため
* `View` と `Router` 以外は **`import UIKit` 禁止**
    * ロジックはUIがどうなっているかを気にしない
* Mockを使ってテスト対象に必要なクラスなどを差し替えることでUnitテスト可能
    * Mockを自作する際はMock自体にバグを仕込まないように注意
    * MockライブラリやMock生成ツールなどを導入することも検討したほうがいい
* ボイラープレートが多いので、ファイルの自動生成ツールを活用して楽をしよう! ([Kuri](https://github.com/bannzai/Kuri)がおすすめ)