33
17

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.

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

Posted at

はじめに

今回が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に書くべきなのかなと思っています.
もっと勉強していきます!!最後まで読んでいただき,ありがとうございました!!

33
17
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
33
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?