post Image
VIPERアーキテクチャ まとめ

VIPERアーキテクチャとは?

VIPER_img

VIPERアーキテクチャの理念

  • 単一責任の原則のもと適切に分割すること

    • ∴「このクラスは〇〇をするクラスである」と一言で簡単に説明ができる
  • VIPERはView, Interactor, Presenter, Entity, Routerの頭文字を取ってVIPERと呼ぶ
    • ※それぞれの詳しい役割やサンプルコードは後述
  • View, Interactor, Presenter, Entity, Routerはそれぞれprotocolを切り、それに準拠した実装を行う

    • 実際にそれぞれを組み合わせる際は、protocolを介してアクセスすること
      protocolにのみ依存している状態にすること
    • これによりそれぞれの要素の差し替えが容易になり、テストコードが書きやすくなる

VIPERそれぞれの役割

関係

relationship

ざっくりと処理の流れ

  1. Routerで画面の生成に必要な依存性を解決させる
  2. 1.で生成された画面を表示
  3. ViewからイベントをPresenterにお知らせする

    • ライフサイクル
    • ユーザのタップ
    • …などなど
  4. PresenterViewから送られてきたイベントの内容に合わせて以下のような処理を実行する

    • Viewに対して画面の更新依頼を投げる

      • Viewは依頼された通りに画面の更新をする
    • Interactorに対してデータの取得依頼を投げる

      • Interactorは依頼されたデータ取得が完了したらPresenterに通知する
    • Routerに対して画面遷移の依頼を投げる

      • Routerは依頼された画面へ遷移する → 1. へ戻る

命名

役割 プロトコル名 実体名
View {ModuleName}View {ModuleName}ViewController
Interactor {ModuleName}Usecase {ModuleName}Interactor
InteractorDelegate {ModuleName}InteractorDelegate {ModuleName}InteractorDelegate
Presenter {ModuleName}ViewPresentation {ModuleName}ViewPresenter
Entity – ※1 {~~}Entity ※2
Router {ModuleName}Wireframe {ModuleName}Router

※1:Entityはテストの際に差し替える必要がないほどシンプルな構造なのでプロトコル不要
※2:1つのモジュールで複数のEntityを扱うこともあるため

それぞれの役割の詳細

以下ではサンプルコードを元に説明
GitHubのAPIをたたいてリポジトリの一覧を取得し表示する

Router

  • 「画面遷移」と「依存関係の解決」を担当
  • VIPERアーキテクチャの肝であり、他の有名アーキテクチャにないところ
    • 他アーキテクチャではViewに画面遷移の処理もお願いする必要があり、Viewが「画面の更新」と「画面遷移」の2つを担当する必要があった
      Viewのコードの見通しが悪くなりがちだった
    • VIPERでは「画面遷移」の処理をRouterに移管したことでViewの責務を減らせた

Routerのサンプル

RepositoryListRouter.swift
import UIKit

class RepositoryListRouter {

    // 画面遷移のためにViewControllerが必要。initで受け取る
    weak var viewController: UIViewController?

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

    // 依存関係の解決をしている
    static func assembleModules() -> UIViewController {
        let view = RepositoryListViewController()
        let interactor = RepositoryListInteractor()
        let router = RepositoryListRouter(viewController: view)
        // PresenterはView, Interactor, Routerそれぞれ必要なので
        // 生成し、initの引数で渡す
        let presenter = RepositoryListViewPresenter(view: view, interactor: interactor, router: router)

        interactor.output = presenter // Interactorの通知先を設定
        view.presenter = presenter    // ViewにPresenterを設定

        return view
    }
}

// Routerのプロトコルに準拠する
// 遷移する各画面ごとにメソッドを定義
extension RepositoryListRouter: RepositoryListWireframe {

    func showRepositoryDetail(_ repository: Repository) {
        // 詳細画面の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のサンプル

RepositoryListViewController.swift
import UIKit

class RepositoryListViewController: UIViewController {

    // Presenterへのアクセスはprotocolを介して行う
    var presenter: RepositoryListViewPresentation!

    @IBOutlet private weak var tableView: UITableView!

    private var cells: [RepositoryListCellType] = [] {
        didSet {
            tableView.reloadData() // 画面の更新

            if refreshControl.isRefreshing {
                refreshControl.endRefreshing()
            }
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        

        presenter.viewDidLoad() // Viewの読み込みが完了したことを通知
    }
}

// Viewのプロトコルに準拠する
extension RepositoryListViewController: RepositoryListView {

    func reloadData(_ data: [RepositoryListCellType]) {
        self.cells = data
    }
}

Presenter

  • Viewから受け取ったイベントを元に「画面の更新処理に関わるロジック」を担当
  • Viewからのイベントの内容によって必要な処理を実施、または別のクラスに依頼する

    • Viewに対して画面の更新依頼を投げる
    • Interactorに対してデータの取得依頼を投げる
    • Routerに対して画面遷移の依頼を投げる
  • Presenterが提供するメソッド名は
    「画面の更新が終わった(viewDidLoad)」「ボタンが押された(hogeButtonDidPush)」
    といった命名にすること

    • ×「(ボタンが押されたので)詳細画面に遷移する(showDetailView)」
  • import UIKit禁止

    • UIがどうなっているかを気にしない

Presenterのサンプル

RepositoryListViewPresenter.swift
import Foundation

class RepositoryListViewPresenter {

    // View, Interactor, Routerへのアクセスはprotocolを介して行う
    private weak var view: RepositoryListView?
    private let interactor: RepositoryListUsecase
    private let router: RepositoryListWireframe

    private var searchText: String = "" {
        didSet {
            guard !searchText.isEmpty else { return }

            // Viewに状態反映
            view?.setLastSearchText(searchText)
            view?.showRefreshView()
            // 検索テキストでAPIを叩く
            interactor.fetchRepositories(keyword: searchText)
        }
    }
    private var repositories: [Repository] = [] {
        didSet {
            guard !repositories.isEmpty else { return }

            // 取得した情報からViewに表示するセルの情報を作成
            cellTypes = repositories.map { repository in
                RepositoryListCellType.repositoryCell(repository: repository)
            }
        }
    }
    private var cellTypes: [RepositoryListCellType] = [] {
        didSet {
            // セルの情報がセットされたらViewに反映
            view?.reloadData(cellTypes)
        }
    }

    init(view: RepositoryListView, interactor: RepositoryListUsecase, router: RepositoryListWireframe) {
        self.view = view
        self.interactor = interactor
        self.router = router
    }
}

// Presenterのプロトコルに準拠する
extension RepositoryListViewPresenter: RepositoryListViewPresentation {

    func viewDidLoad() {
        interactor.loadLastSearchText() // Interactorにデータ取得処理を依頼
    }

    func didSelectRow(at indexPath: IndexPath) {
        guard indexPath.row < repositories.count else { return }

        let repository = repositories[indexPath.row]
        router.showRepositoryDetail(repository) // Routerに画面遷移を依頼
    }

    
}

// Interactorからの通知に関するプロトコルに準拠する
extension RepositoryListViewPresenter: RepositoryListInteractorDelegate {

    func interactor(_ interactor: RepositoryListUsecase, lastSearchTextLoadState state: SearchTextLoadState) {
        // ローカルから読み込んだ検索テキストを受け取る
        // テキストをセットするとリポジトリの取得が走る
        switch state {
        case .result(let searchText):
            self.searchText = searchText
        case .error:
            cellTypes = [.noHistoryCell]
        }
    }

    func interactor(_ interactor: RepositoryListUsecase, didFetchedRepositories repositories: [Repository]) {
        // 取得したリポジトリの配列をセットすることでプロパティ監視が発火して
        // 最終的にViewに反映される
        self.repositories = repositories
    }
}

Interactor

  • 「データ関わるロジック」を担当(取得、加工、保存など)
  • Presenterから依頼されたデータを取得し返す

    • 取得が完了したらInteractorDelegate経由してPresenterに通知
      • 必ずDelegate経由で返す
      • 戻り値で返すとメソッドの数が多くなる
    • WebAPI、バンドルされたファイル、ローカルに保存されているデータなど
  • import UIKit禁止

    • UIがどうなっているかを気にしない

Interactorのサンプル

RepositoryListInteractor.swift
import Foundation

class RepositoryListInteractor {

    // 取得処理が完了したことを通知はprotocolを介して行う
    weak var output: RepositoryListInteractorOutput?

    private var repositories: [Repository] = []
}

// Interactorのプロトコルに準拠する
extension RepositoryListInteractor: RepositoryListUsecase {

    func fetchRepositories(keyword: String) {
        let request = GitHubAPI.SearchRepositories(keyword: keyword)

        let client = GitHubClient()
        client.send(request: request) { result in
            switch result {
            case .success(let response):
                DispatchQueue.main.async {
                    // 取得完了したことを通知
                    self.delegate?.interactor(self, didFetchedRepositories: response.items)
                }
            case .failure(let error):
                DispatchQueue.main.async {
                    self.delegate?.interactor(self, didFailedWithError: error)
                }
            }
        }
    }
}

Entity

  • 「データ構造の定義」を担当
  • structでデータ構造を定義する
  • import UIKit禁止

    • UIがどうなっているかを気にしない
  • 基本ロジックを持たないようにする(※Entityにロジックを持つとテストが書きづらくなるため)
    以下のみを定義する

    • プロパティ
    • init

Entityのサンプル

Repository.swift
import Foundation

struct Repository: Decodable {

    let id: Int
    let name: String
    let fullName: String
    let htmlURL: URL
    let starCount: Int
    let owner: User

    enum CodingKeys: String, CodingKey {
        case id
        case name
        case fullName = "full_name"
        case htmlURL = "html_url"
        case starCount = "stargazers_count"
        case owner
    }
}

ディレクトリ構造

├── application
│   ├── AppDelegate.swift
│   ├── Base.lproj
│   │   └── LaunchScreen.storyboard
│   ├── Info.plist
├── modules  // modules配下にモジュールを作成する
│   ├── Root
│   │   └── RootRouter.swift
│   ├── RepositoryList
│   │   ├── Interactor
│   │   │   └── RepositoryListInteractor.swift
│   │   ├── Interface
│   │   │   └── RepositoryListInterface.swift
│   │   ├── Presenter
│   │   │   └── RepositoryListPresenter.swift
│   │   ├── Router
│   │   │   └── RepositoryListRouter.swift
│   │   └── View
│   │       ├── RepositoryListViewController.swift
│   │       ├── RepositoryListViewController.xib
│   │       └── SubView
│   │           ├── RepositoryResultCell.swift
│   │           └── RepositoryResultCell.xib
│   ├── ...
├── enitities // 公式ではmodelという命名だが、entityのほうが誤解がないのでこうしている
│   ├── Repository.swift
│   ├── ...
├── extensions
│   ├── UIView+extension.swift
│   ├── ...
├── resources
│   └── Assets.xcassets
│       ├─...

Unitテスト

  • 流れ
    • テストしたい対象が依存しているクラスなどのMockを作成
    • ↑で作ったクラスなどを使ってテストしたい対象を初期化
    • テストしたい対象のメソッドを呼び出すことで起こる挙動を確認
  • 注意点
    • Mockを自作することで、Mock自体にバグが発生する可能性がある
    • protocolに1つメソッドを追加するだけでもテストコードを直す必要があり、大変
    • Mockライブラリを使うことで解決できることが多い

PresenterのUnitテストのサンプル

RepositoryListPresenterTest.swift
import XCTest

class RepositoryListPresenterTest: XCTestCase {

    // 依存するクラスの初期化
    let view = ViewMock()
    let interactor = InteractorMock()
    let router = RouterMock()
    var presenter: RepositoryListViewPresenter!

    override func setUp() {
        super.setUp()

        presenter = RepositoryListViewPresenter(view: view, interactor: interactor, router: router)
    }

    func test_viewDidLoad() {
        // PresenterにviewDidLoadのイベントが届いたときの挙動をテスト
        // Interactorにリポジトリ一覧を取得するよう依頼する実装にしたので、
        // 正しく依頼されているかチェック
        XCTAssertFalse(interactor.isCalled_fetchRepositories)
        presenter.viewDidLoad()
        XCTAssertTrue(interactor.isCalled_fetchRepositories)
    }

    func test_fetchRepositoriesDidFinish() {
        // Presenterにリポジトリ一覧の取得完了イベントが届いたときの挙動テスト
        // イベントを受け取ったらViewに再描画を依頼するよう実装したので、
        // 正しく依頼されているかチェック
        XCTAssertFalse(view.isCalled_reloadData)
        presenter.fetchRepositoriesDidFinish()
        XCTAssertTrue(view.isCalled_reloadData)
    }
}

モックは以下のように作る

Viewのモック
// Viewのprotocolに準拠
class ViewMock: RepositoryListView {

    var isCalled_reloadData = false

    func reloadData() {
        // メソッドが呼ばれたことを記録
        isCalled_reloadData = true
    }
}

まとめ

  • 単一責任の原則をもとに適切にクラス分割すること
  • 依存性の解決はRouterで行い、クラス内部でinit()等をしないこと
  • 分割されたそれぞれのクラスはprotocolを介してアクセスすること
    • Unitテストの際に差し替えできるようにするため
  • ViewRouter以外はimport UIKit禁止

    • UIがどうなっているかを気にしない
  • Mockを使ってテスト対象に必要なクラスなどを差し替えることでUnitテスト可能
    • Mockを自作する際はMock自体にバグを仕込まないように注意
    • Mockライブラリを導入することも検討したほうがいい

『 Swift 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。