この記事は、NIFTY Advent Calendar 2017の2日目の記事です
1日目はwinterwind26さんの「[PHP] LINE Messaging API を使ったチャットボットをテストしてみた」でした。

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

前段

MVPとは

mvp_image.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にあげてます
※急いでたのでコミットメッセージが雑です、お許しを

MVPそれぞれの役割

おさらい
mvp_image.png

View

  • UIView / UIViewController
  • Modelを直接いじらない
  • Presenterにイベントを移譲
  • Presenterから貰った処理結果をそのまま表示するだけ
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がどうなっているかを考慮しない
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がどうなっているかを考慮しない
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さんの記事です、お楽しみに!