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