LoginSignup
46
44

More than 5 years have passed since last update.

ぼくのやっているVIPER(のようなもの)

Last updated at Posted at 2017-09-07

VIPER関連の記事を幾つか読み、モチベーションが高まったのでアプリを作り始めました:muscle:
VIPERについてちゃんと理解したわけではないので、巷で言われているものと違いがあるかもしれませんが、これでもやれてますという一例として見てもらえればと思います:pray:
間違い指摘、ご意見、アドバイス、感想等ありましたらぜひコメントお願いします:bow:

参考にしたもの

以下の記事を読みました。サンプルもあったので、ディレクトリ構成やクラス名は同じようにしています。
iOS Project Architecture: Using VIPER
サンプルのリポジトリ→https://github.com/pedrohperalta/Articles-iOS-VIPER
日本語が良ければ→iOS Project Architecture : Using VIPER [和訳]

ディレクトリ構成

ほぼ上のリポジトリの通りです。Utilitiesだけ勝手に付け足しました。

.
├── Application
│   ├── AppDelegate.swift
│   └── Bridging-Header.h
├── Enums
│   └── 各Enum
├── Extensions
│   └── 各クラス拡張
├── Models
│   └── 各エンティティクラス(何故かディレクトリの名前はモデル)
├── Mudules
│   ├── 各モジュール名(画面ごとに作る)
│   │   ├── Contract
│   │   │   └── [モジュール名]Contract.swift
│   │   ├── Interactor
│   │   │   └── [モジュール名]Interactor.swift
│   │   ├── Presenter
│   │   │   └── [モジュール名]Presenter.swift
│   │   ├── Router
│   │   │   └── [モジュール名]Router.swift
│   │   └── View
│   │       ├── [モジュール名]ViewController.storyboard
│   │       ├── [モジュール名]ViewController.swift
│   │       ├── Cell
│   │       │   └── 画面で使うセルのxibとswift
│   │       └── View
│   │           └── 画面で使うビューのxibとswift
│   └── Root(AppDelegateから呼ぶ用)
│       ├── RootContract.swift
│       └── RootRouter.swift
├── Protocols
│   └── ビューのプロトコルなど
├── Resources
│   ├── Images.xcassets
│   └── Info.plist
└── Utilities
    ├── View
    │   └── いくつかの画面で使いまわすセルとかビューとか
    └── Interactor
        └── APIやDBを叩くための便利クラス

Mudules以下を作るときはGenerambaというコードジェネレータを使うと便利です。使い方は以下の記事で。
iOS開発で VIPER / Clean Architecture を使うなら、ファイル自動生成の Generamba もどうぞ
デフォルトでVIPER向けのテンプレートが入っているのですが、プロトコルの名前がちょっと違ったり、分け方が細かったりしたので、自分でテンプレートを作ってそれを使っています。
テンプレートの作り方についてはそのうちまとめたい。。。
自作したもの: https://github.com/Yaruki00/GenerambaTemplate

役割

Rootモジュール

AppDelegateから呼ばれます。windowのrootViewControllerをセットします。

AppDelegate
class AppDelegate: UIResponder {
    var window: UIWindow?
}

extension AppDelegate: UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.window = UIWindow(frame: UIScreen.main.bounds)
        RootRouter().presentFirstScreen(in: window!)
        return true
    }
}
RootRouter
class RootRouter: RootWireframe {

    func presentFirstScreen(in window: UIWindow) {
        window.makeKeyAndVisible()
        window.rootViewController = HogeRouter.assembleModule()
    }
}

プロトコル

Viewの共通処理なんかはプロトコルにまとめておき、デフォルト実装を書いておくと便利です。
今のところViewのプロトコルしかないですが、他にも作った方がいいものがあるかもしれません。

次に例を示します。

ViewProtocols
protocol APICallableView: IndicatableView, NetworkErrorShowableView {}

protocol IndicatableView: class {
    func showActivityIndicator()
    func hideActivityIndicator()
}

extension IndicatableView where Self: UIViewController {

    func showActivityIndicator() {
        // インジケータを表示する処理
    }

    func hideActivityIndicator() {
        // インジケータを隠す処理
    }
}

protocol NetworkErrorShowableView: class {
    func presentNetworkErrorAlert()
}

extension NetworkErrorShowableView where Self: UIViewController {

    func presentNetworkErrorAlert() {
        UIAlertController.showOkAlert( // UIAlertControllerのExtensionでこんなのを作っています
            vc      : self,
            title   : "ネットワークエラー",
            message : "接続状態を確認してください"
        )
    }
}

各モジュールにあるもの

Contract

View/Presenter/Interactor/Routerのプロトコルをまとめて書きます。
プロトコルの種類と用途は以下の通りです。

プロトコル名 実装するクラス プロパティ メソッド
[モジュール名]View ViewController presenter Presenterから呼ばれるビュー更新メソッド
[モジュール名]Presentation Presenter view, interactor, router Viewで発生したイベントを受け取るメソッド
[モジュール名]InteractorOutput Presenter - Interactorでのデータ取得の結果を受け取るメソッド
[モジュール名]UseCase Interactor output Presenterから呼ばれるAPIやDBなどからのデータ取得メソッド
[モジュール名]Wireframe Router viewController Presenterから呼ばれる画面遷移メソッド

次に例を示します。

Contract
// ViewControllerが実装
protocol [モジュール名]View: APICallableView {
    var presenter: [モジュール名]Presentation! { get set }

    // Presenterから呼ばれる
    // ビューの更新
    func showSomeData(_ data: [SomeData])
}

// Presenterが実装
protocol [モジュール名]Presentation: class {
    weak var view: [モジュール名]View? { get set }
    var interactor: [モジュール名]UseCase! { get set }
    var router: [モジュール名]Wireframe! { get set }

    // Viewから呼ばれる
    // Viewで起きたイベントを通知
    func dataWillReload()
    func dataCellDidTap(_ data: SomeData)
}

// Presenterが実装
protocol [モジュール名]InteractorOutput: class {
    // Interactorから呼ばれる
    // Interactorがデータの取得に成功/失敗したことの通知
    func dataFetched(_ data: [SomeData])
    func dataFetchFailed(error: Error)
}

// Interactorが実装
protocol [モジュール名]UseCase: class {
    weak var output: [モジュール名]InteractorOutput! { get set }

    // Presenterから呼ばれる
    // APIやDBなどからのデータ取得
    func fetchData()
}

// Routerが実装
protocol [モジュール名]Wireframe: class {
    weak var viewController: UIViewController? { get set }

    // 画面遷移の際、遷移元のRouterから呼ばれる
    static func assembleModule() -> UIViewController

    // Presenterから呼ばれる
    // 画面遷移
    func pushDataDetail(data: SomeData)
}

いくつか処理の流れを書くと以下のようになります。

例1) 初回起動でAPIからデータ取得して表示
ViewController.viewDidLoad()
→ Presentation.dataWillReload()
→ UseCase.fetchData()
→ InteractorOutput.dataFetched(_:)
→ View.showSomeData(_:)

例2) セルタップで詳細画面へ遷移
UITableViewDelegate.tableView(_:didSelectRowAt:)
→ Presentation.dataCellDidTap(_:)
→ Wireframe.pushDataDetail(data:)

View

EntityをPresenterから受け取って、それを元に見た目の更新をします。
表示しているEntityや、自身の状態をプロパティとして持っています。
クラスはUIViewControllerかUIViewのそれ自身、あるいは派生クラスです。
EntityはViewまで渡ってこないという記事があったと思うのですが、Viewが持っていた方がセルの生成だったり、Presenterへイベント通知とともにデータを渡したりするのに都合がいいので、ViewにEntityを持たせています。

次に例を示します。

View
// MARK: vars and life cycle
class HogeViewController: UIViewController {

    @IBOutlet weak var typeSegmentControl: UISegmentedControl!
    @IBOutlet weak var tableView: UITableView!

    var presenter: HogePresentation!

    fileprivate var dataList : [SomeData] = [] // 表示するEntity
    fileprivate var currentSegmentIndex = 0    // 自身の状態(この例だとあまり持つ意味が無いけど)

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setupUI()
        self.presenter.dataWillReload() // ビューがロードされたことをPresenterに通知
    }
}

// MARK: - setup UI
extension HogeViewController {

    fileprivate func setupUI() {
        // 各ビューのセットアップ
    }
}

// MARK: - UI operation
extension HogeViewController {

    @IBAction func typeDidSelect(_ sender: UISegmentedControl) {
        self.currentSegmentIndex = sender.selectedSegmentIndex // 状態が変化したら更新
        self.tableView.reloadData()
    }
}

// MARK: HogeView
extension HogeViewController: HogeView {

    func showSomeData(_ data: [SomeData]) {
        self.dataList = songList    // 返ってきたEntityを保持しておく
        self.tableView.reloadData() // ビューの更新
    }
}

// MARK: - UITableViewDataSource
extension ITunesRankingViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.dataList.count // 持っているEntityを元に計算
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: ITunesRankingCell.name, for: indexPath) as! SomeDataCell
        cell.setData(self.dataList[indexPath.row]) // 持っているEntityを渡す
        return cell
    }
}

// MARK: - UITableViewDelegate
extension ITunesRankingViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.presenter.dataCellDidTap(self.dataList[indexPath.row]) // セルがタップされたことをPresenterに通知
    }
}

Presenter

Viewで発生したイベント(とあればそのイベントに紐づくEntity)を受け取り、InteractorまたはRouterへの橋渡しをします。
また、Interactorから返ってきたEntityをViewに渡したり、Interactorに処理を依頼中にViewにインジケータを出させたりします。
Entityや状態は一切保持しません。

次に例を示します。

Presenter
class HogePresenter: HogePresentation {

    weak var view: HogeView?
    var interactor: HogeUseCase!
    var router: HogeWireframe!

    func dataWillReload() { // イベント受け取る
        self.view?.showActivityIndicator() // Viewにインジケータを出させる
        self.interactor.fetchData()        // Interactorに橋渡し
    }

    func dataCellDidTap(_ data: SomeData) { // イベント受け取る
        self.router. pushDataDetail(data: data) // Routerに橋渡し
    }
}

extension HogePresenter: HogeInteractorOutput {

    func dataFetched(_ data: [SomeData]) { // InteractorからEntityが返ってくる
        self.view?.showSomeData(data)      // ViewにEntity渡す
        self.view?.hideActivityIndicator() // Viewにインジケータ消させる
    }

    func dataFetchFailed(error: Error) { // InteractorでEntity取得失敗
        self.view?.hideActivityIndicator()    // Viewにインジケータ消させる
        self.view?.presentNetworkErrorAlert() // Viewにアラート出させる
    }
}

Interactor

Presenterからの依頼を受けてAPIやDBなどからデータ取得を行い、その結果をEntityにして返します。
実際にAPIやDBを叩く処理は直接書くのではなく、便利クラスを挟むのが良いと思います。

次に例を示します。

Interactor
class HogeInteractor: HogeUseCase {

    weak var output: HogeOutput!

    func fetchData() {
        AlamofireManager.shared.request( // Alamofireを使うための便利クラス
            url     : "https://some_datasource/path/file",
            success : { json in // JSONが返ってくる
                let data = SomeData.createList(from: json) // Entityの生成
                self.output?.dataFetched(data)
        },
            fail    : { error in // Errorが返ってくる
                self.output?.rankingListFetchFailed(error)
        })
    }
}

Router

Presenterからの依頼を受けて画面遷移を行います。そのためにViewControllerをプロパティとして持っています。
また、画面遷移をしてくる際に一番初めに呼ばれ、View/Presenter/Interactor/Routerをインスタンス化し各プロパティをセットするメソッドを持っています。

次に例を示します。

Router
class HogeRouter: HogeWireframe {

    weak var viewController: UIViewController?

    static func assembleModule() -> UIViewController {
        let view = HogeViewController.instantiate() // UIViewControllerのExtensionでこんなの作ってます
        let presenter = HogePresenter()
        let interactor = HogeInteractor()
        let router = HogeRouter()

        view.presenter = presenter

        presenter.view = view
        presenter.interactor = interactor
        presenter.router = router

        interactor.output = presenter

        router.viewController = view

        return view
    }

    func pushDataDetail(data: SomeData) {
        let nextVC = HogeDetailRouter.assembleModule(data: data)
        self.viewController?.navigationController?.pushViewController(nextVC, animated: true)
    }
}

Entity

Viewで使うデータを持つクラスです。jsonなどからEntityに変換できるメソッドなりイニシャライザーがあると便利です。

次に例を示します。

Entity
class SomeData {

    var id   : String
    var name : String = ""
    var num  : Int = 0

    init(json: JSON) { // SwiftJsonとか使っている前提
        self.id   = json["id"].string ?? ""
        self.name = json["info"]["name"].string ?? ""
        self.num  = json["info"]["num"].int ?? 0
    }

    static func createList(from json: JSON) -> [SomeData] {
        return (json["items"].array ?? []).map { json -> SomeData in return SomeData(json: json) }
    }
}

おわりに

いかがだったでしょうか?いいなと思うところ、それは違うんじゃないかと思うところ、どちらもあったのではないでしょうか?:smirk_cat:
僕は今回この方法で上手く行っていますが、アーキテクチャは日々進化しているし、対象とするアプリの規模や種類によって最適なものは変わると思っています:runner:
そんな中で、この記事が少しでも考えるきっかけ、議論の種になってくれたらいいなと思います:information_desk_person:

46
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
44