【Swift】MVCから脱却したいのでMVPの勉強をした

この記事は、[NIFTY Advent Calendar 2017](https://qiita.com/advent-calendar/2017/nifty)の2日目の記事です
1日目は[winterwind26](https://qiita.com/winterwind26)さんの「[[PHP] LINE Messaging API を使ったチャットボットをテストしてみた](https://qiita.com/winterwind26/items/7b0755f581cbe8c9e4f1)」でした。

ニフティでモバイルアプリエンジニア(iOSメイン)をしています、hicka04です。
※実はこの記事がQiita初投稿なので、温かい目で見守っていただけると幸いです。
今回は、自分が担当しているiOSアプリの改善をしたくて勉強したことを共有しようと思います。

# 前段
## MVPとは
![mvp_image.png](https://qiita-image-store.s3.amazonaws.com/0/80832/d9a2aef2-4b22-72f8-c8cf-0552d6e44d23.png)

* Model - View - Presenterからなるアーキテクチャ
* MVVMやClean Architectureなど、さまざまなアーキテクチャのうちの一つ

## なぜMVCから脱却したいのか?
* ViewControllerの肥大化
  * いわゆるFatViewController / MassiveViewController
  * iOSアプリではController≒ViewController  
    →View要素とController要素の2つの持つため、**役割がわかりづらくなってしまう**
* テストが書きづらい
  * 依存関係が絡み合ってモックとか作れない

## なぜ今回MVPを検討したか?
* MVP以外にもMVVMやClean ArchitectureやVIPERなどがありますが
    * 既存コードからの変更内容は多くしたくない  
      →新たに増えるファイルが少なそうなのは MVP or MVVM
    * RxSwiftの学習コスト高そうだし、教えられなさそう  
      →MVP \やってみよう!/

# 実装してみた
## デモアプリ
* 天気のAPIを叩いてTableViewに表示
* セルをタップしたら詳細ページに遷移する

コードは[Github](https://github.com/hicka04/mvp-prac)にあげてます
※急いでたのでコミットメッセージが雑です、お許しを

## MVPそれぞれの役割
おさらい
![mvp_image.png](https://qiita-image-store.s3.amazonaws.com/0/80832/d9a2aef2-4b22-72f8-c8cf-0552d6e44d23.png)

### View
* UIView / **UIViewController**
* Modelを直接いじらない
* Presenterにイベントを移譲
* Presenterから貰った処理結果をそのまま表示するだけ

```Swift:WeatherListViewController.swift
import UIKit

class WeatherListViewController: UIViewController {

    private(set) var presenter: WeatherListViewPresenter!
    …

    override func viewDidLoad() {
        super.viewDidLoad()
        
        presenter = WeatherListViewPresenter(view: self)
        // setup tableview
        …

        updateWeathers()
    }
    …

    @objc func updateWeathers() {
        // Presenterに取得処理を移譲
        presenter.updateWeathers()
        tableView.refreshControl?.beginRefreshing()
    }
    …
}

extension WeatherListViewController: WeatherListViewProtocol {
    // Presenterから呼ばれる処理
    // データのfetch完了後にTableViewのリロード
    func reloadData() {
        tableView.refreshControl?.endRefreshing()
        
        tableView.reloadData()
    }
    …
}
```

### Presenter
* Viewから受け取ったイベントをもとにModelに処理を移譲
* Modelの処理結果を受け取り、Viewに通知
* **`import UIKit`禁止**
    * UIがどうなっているかを考慮しない

```Swift:WeatherListViewPresenter.swift
import Foundation

class WeatherListViewPresenter: WeatherListViewPresenterProtocol {

    private let view: WeatherListViewProtocol
    …

    required init(view: WeatherListViewProtocol) {
        self.view = view
        self.model = WeatherModel(api: WeatherRESTAPI.shared)
        model.addObserver(self, selector: #selector(self.updated))
    }

    // Modelに処理を移譲
    func updateWeathers() {
        model.resetWeathers()
        // api request
        model.fetchWeathers()
    }

    // Modelから更新されたと通知があったらViewに更新を依頼する
    @objc private func updated() {
        view.reloadData()
    }
    …
}
```

### Model
* ビジネスモデルとかを担当
* Presenterから移譲された処理を実行
    * データの取得とか
        * API使うなら**APIクラスを外からもらう**
* 処理完了後にpresenterに通知
* **`import UIKit`禁止**
    * UIがどうなっているかを考慮しない

```Swift:WeatherModel.swift
import Foundation
import SwiftyJSON

class WeatherModel: WeatherModelProtocol {
    private let api: WeatherAPI
    …
    
    required init(api: WeatherAPI) {
        self.api = api
        …
    }

    func fetchWeathers() {
        // api request...
        api.fetchWeathers { (json) in
            if let jsonArray = json["list"].array {
                jsonArray.forEach({ (data) in
                    let dt = data["dt_txt"].string!
                    let weather = data["weather"][0]["main"].string!
                    self.weathers.append(WeatherEntity(dateString: dt, weather: weather))
                })
                
                // NotificationCenterを使ってPresenterに通知してます
                self.notify()
            }
        }
    }
    …
}
```

# 所感
## よかったこと
* 新たにPresenterを加えたことで「この処理はとりあえずPresenterに渡しておこう」という思考が働いた  
    →ViewControllerから**処理を減らせそうな予感あり**
* 現状のコード(MVC)にPresenterを足すだけなので、移行も大掛かりではなさそう
* テストがかけそう
    * ViewControllerからロジックを引き離せたので、**Mockを作れそう**
    * ビジネスロジックが記述されているModelに対して、  
        **APIクラスを外から渡すことでテストしたいJSONデータを簡単に用意できる**
    * ※これからテストを書くので進捗あれば更新します

## 悩み
* ViewとModelが離れたため、「このアクションがあったときに、どんな処理がされるのか」を追うのは少し大変になった気がする(慣れの問題なのかも)
* protocolを定義する必要があるが、**命名が難しい**
    * 今回は`{クラス名}Protocol`にしたが、`init(view: ViewProtocol)`みたいになるので、  
      実際に受け取りたいもの(View)と引数の型名(ViewProtocol)に差があって違和感がある
    * これが解決すれば、今すぐにでも担当アプリに組み込みたい

# 最後に
明日は[nyangu](https://qiita.com/nyangu)さんの記事です、お楽しみに!