125
104

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-02

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

125
104
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
125
104

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?