mvc
Swift
MVP
architecture

これまで自分が実装してきた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に書くべきなのかなと思っています.


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