VIPER関連の記事を幾つか読み、モチベーションが高まったのでアプリを作り始めました
VIPERについてちゃんと理解したわけではないので、巷で言われているものと違いがあるかもしれませんが、これでもやれてますという一例として見てもらえればと思います
間違い指摘、ご意見、アドバイス、感想等ありましたらぜひコメントお願いします
参考にしたもの
以下の記事を読みました。サンプルもあったので、ディレクトリ構成やクラス名は同じようにしています。
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をセットします。
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
}
}
class RootRouter: RootWireframe {
func presentFirstScreen(in window: UIWindow) {
window.makeKeyAndVisible()
window.rootViewController = HogeRouter.assembleModule()
}
}
プロトコル
Viewの共通処理なんかはプロトコルにまとめておき、デフォルト実装を書いておくと便利です。
今のところViewのプロトコルしかないですが、他にも作った方がいいものがあるかもしれません。
次に例を示します。
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から呼ばれる画面遷移メソッド |
次に例を示します。
// 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を持たせています。
次に例を示します。
// 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や状態は一切保持しません。
次に例を示します。
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を叩く処理は直接書くのではなく、便利クラスを挟むのが良いと思います。
次に例を示します。
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をインスタンス化し各プロパティをセットするメソッドを持っています。
次に例を示します。
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に変換できるメソッドなりイニシャライザーがあると便利です。
次に例を示します。
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) }
}
}
おわりに
いかがだったでしょうか?いいなと思うところ、それは違うんじゃないかと思うところ、どちらもあったのではないでしょうか?
僕は今回この方法で上手く行っていますが、アーキテクチャは日々進化しているし、対象とするアプリの規模や種類によって最適なものは変わると思っています
そんな中で、この記事が少しでも考えるきっかけ、議論の種になってくれたらいいなと思います