これまで自分が実装してきたMVPは中途半端だった


はじめに

今回がQiita初投稿です

普段は,Swift・Python・Goあたりを触っている執筆者です

生暖かい目線でお願いします

執筆したきっかけ

  • MVVMやFluxを学ぶ前に,MVCとMVPを復習した
  • iOSアプリ設計パターンを読んだ結果,悔い改めることになったため
  • 備忘録として残したかったから

読者に求めること

  • iOSエンジニアである必要はありません
  • ただし,Appleにおける○○という表現はします
  • アーキテクチャって?とかアーキテクチャ...なるほどよくわからん って方(一緒に勉強していきましょう!!)
  • iOSアプリ設計パターンを手元に置きながら,読んでほしいです!

MVC

あまり多くは触れませんが,MVPを述べるためには必要不可欠な知識であるため書きます

AppleにおけるMVCは少し特殊です.原初MVCと何が違うかについては,表を示した上で説明します.

原初MVC

レイヤー 役割
Controller ユーザの入力を受けて,Modelに変更指示を送る
Model 指示を受けて,自身を更新する
View Modelの変更を監視して,検知したら自身を更新する

特徴として,iOSアプリ設計パターンでは以下のように述べられています.

  • ウィジェット単位でプレゼンテーションとロジックを分ける
  • Modelの変更に対し,オブザーバー同期が行われる

AppleにおけるMVC

レイヤー 役割
Controller ユーザの入力を受けて,Modelに変更指示を送る.
Modelの変更を監視して,検知したらViewを更新する.
Model 指示を受けて,自身を更新する
View 画面の描画担当

大きな違いはViewとModelの関係でしょうか.原初MVCではView自身がModelを監視していましたが,AppleのMVCでは,Modelの監視はControllerの役割です.

このようになった背景として,iOSアプリ設計パターンでは以下のように述べられています.

複雑なGUIやタッチスクリーンの登場により,「入力」と「出力」の境界線が曖昧になってきた.また,複雑になってきたUIに一貫性を持たせるためには,Viewの再利用が不可欠である.さらに,ViewとModelを完全分離したいという声も出てきた

このような背景から,ViewControllerとなったのでしょうか...?(まだ曖昧です)
なんにしても,xibによってUIViewやTableCellが表現できることも,上記のような要因がありそうですね

MVP

ここまできて,ようやくMVPの話になります.

レイヤー 役割
Presenter ユーザの入力を受けて,Modelに変更指示を送る.
Modelからのコールバックを受けて,Viewを更新する.
Model 指示を受けて,自身を更新する
変更後,コールバックなどを行う
View 画面の描画担当

ん?AppleにおけるMVCと酷似していますよね,そうなんです.これについてiOSアプリ設計パターンでは以下のように述べられています.

設計変更の動機,レイヤーの役割のどちらから見てもこれはMVP(Passive View)と同じパターンだと考えてよいでしょう

MVPを採用するメリットは?

これに対して私は,以下のように考えています.
- ViewControllerの責務を分けられる
- テストが書きやすい
- protocolによって,レイヤー間の依存性を薄めることができる
- 個人,学生チームによるアプリ開発の規模感にはMVVM以上はオーバーなことが多い
- iOSに限ってですが,アーキテクチャって?の人が学ぶ1つ目にはちょうどいい複雑さ

悔い改める必要があったポイント

  • protocolによって,レイヤー間の依存性を薄めることができる

主にこの部分です.後はModelの意味合いです.

protocolによって,レイヤー間の依存性を薄めることができる

MVCはオブザーバー同期に対して,MVPはフロー同期です.(Supervising Controllerについては割愛させていただきます)そのため,デザインパターンの1つであるDelegateを使用して,レイヤー間に関係を持たせます.

なお,Delegateについては,【swift】イラストで分かる!具体的なDelegateの使い方。がとてもわかり易いです!!

しかし,自分がこれまで書いてきたコードを見ると,中途半端であることがわかりました

SampleViewController.swift
import UIKit

protocol SampleProtocol: class {
    func reloadFeed()
}

class SampleViewController: UIViewController {

    private var presenter: SamplePresenter!

    override func viewDidLoad() {
        super.viewDidLoad()
        setHoge: do {
            presenter = SamplePresenter(view: self)
            presenter.callGetSample()
        }
    }


}

extension SampleViewController: SampleProtocol {

    func reloadFeed() {
        print("hoge")
    }

}
SamplePresenter.swift
import Foundation

struct SampleModel: Codable {
    let name: String
    let email: String
}


final class SamplePresenter {
    typealias View = SampleProtocol & SampleViewController
    private var state: loadStatus = .initial
    private weak var view: View?
    private var contentsList: [SampleModel] = []

    var numberOfSampleModel: Int {
        return contentsList.count
    }

    init(view: View) {
        self.view = view
    }

    func sample(at index: Int) -> SampleModel? {
        guard index < contentsList.count else { return nil }
        return contentsList[index]
    }

    func callGetSample() {
        defer {
            DispatchQueue.main.async {
                self.view?.reloadFeed()
            }
        }
        getSample(after: { str in
            self.contentsList = str
        })
    }

    func callPostSample() {
        defer {
            DispatchQueue.main.async {
                self.view?.reloadFeed()
            }
        }
        postSample(after: { str in
            self.contentsList = str
        },body: "hoge")
    }


    private func getSample(after: @escaping ([SampleModel]) -> Void) {

    }

    private func postSample(after: @escaping ([SampleModel]) -> (), body: String) {

    }

}

上記のコードにはたくさんの問題があります.

- View(ViewController)が直接 Presenterのメソッドを呼んでいる
- Presenter内にAPI通信を行うメソッドが書かれている

他にもたくさんありますが,大きな問題は上記だと思います.

そこで,本を読んだ上で以下のように修正してみました.

SampleViewController.swift
import UIKit


class SampleViewController: UIViewController {

    @IBOutlet weak var label: UILabel!

    let model = SampleModel()
    private var presenter: SamplePresenterInput!

    override func viewDidLoad() {
        super.viewDidLoad()
        presenter = SamplePresenter(view: self, model: model)
        label.text = "test"

    }

    @IBAction func onGetTapped(_ sender: Any) {
        presenter.onGetTapped()
    }

    @IBAction func onPostTapped(_ sender: Any) {
        presenter.onPostTapped(body: label.text!)
    }

}

extension SampleViewController: SamplePresenterOutput {

    func setName(name: String) {
        print("setName")
    }

    func setEmail(email: String) {
        print("setEmail")
    }
}

SamplePresenter.swift
import Foundation

protocol SamplePresenterInput: class {
    var numberOfSampleModel: Int { get }
    func user(at index: Int) -> UserModel?
    func onGetTapped()
    func onPostTapped(body: String)
}

protocol SamplePresenterOutput: class {
    func setName(name: String)
    func setEmail(email: String)
}

final class SamplePresenter: SamplePresenterInput {

    private weak var view: SamplePresenterOutput!
    private var model: SampleModelInput
    private var contentsList: [UserModel] = []

    init(view: SamplePresenterOutput, model: SampleModelInput) {
        self.view = view
        self.model = model
    }

    var numberOfSampleModel: Int {
        return contentsList.count
    }

    func user(at index: Int) -> UserModel? {
        guard index < contentsList.count else { return nil }
        return contentsList[index]
    }

    func onGetTapped() {
        model.getSample(completion: { result in
            self.view.setName(name: result.name)
            self.view.setEmail(email: result.email)
        })
    }

    func onPostTapped(body: String) {
        model.postSample(completion: { result in
            self.view.setName(name: result.name)
            self.view.setEmail(email: result.email)
        }, body: body)
    }

}

SampleModel.swift
import Foundation

protocol SampleModelInput {
    func getSample(completion: @escaping (UserModel) -> ())
    func postSample(completion: @escaping (UserModel) -> (), body: String)
}

struct UserModel: Codable {
    let name: String
    let email: String
}

final class SampleModel: SampleModelInput {

    let testUser = UserModel(name: "bob", email: "bob@hogehoge")

    func getSample(completion: @escaping (UserModel) -> ()) {
        print("get")
        completion(testUser)
    }

    func postSample(completion: @escaping (UserModel) -> (), body: String) {
        print("post")
        completion(testUser)
    }

}

UILabel周りとか,APIを想定しているのにgetとpostがほほ同じことは目をつぶってください

注目して欲しい箇所は,各レイヤーがprotocolによって繋がれていて,疎結合になっていることです.

互いをほぼ意識することなく,テストが書きやすくなっているはずです.

また,適切な役割をModelに担わせることができました.

最後に

まだ理解できていない箇所があります.

let model = SampleModel()
private var presenter: SamplePresenterInput!

override func viewDidLoad() {
    super.viewDidLoad()
    presenter = SamplePresenter(view: self, model: model)
    label.text = "test"

}

この部分です.特にModelに関する記述は.ViewではなくPresenterに書くべきなのかなと思っています.

もっと勉強していきます!!最後まで読んでいただき,ありがとうございました!!