Edited at
NIFTYDay 2

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

More than 1 year has passed since last update.

この記事は、NIFTY Advent Calendar 2017の2日目の記事です

1日目はwinterwind26さんの「[PHP] LINE Messaging API を使ったチャットボットをテストしてみた」でした。

ニフティでモバイルアプリエンジニア(iOSメイン)をしています、hicka04です。

※実はこの記事がQiita初投稿なので、温かい目で見守っていただけると幸いです。

今回は、自分が担当しているiOSアプリの改善をしたくて勉強したことを共有しようと思います。


前段


MVPとは


  • 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それぞれの役割

おさらい


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さんの記事です、お楽しみに!