はじめに
今回が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の使い方。がとてもわかり易いです!!
しかし,自分がこれまで書いてきたコードを見ると,中途半端であることがわかりました
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")
}
}
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通信を行うメソッドが書かれている
他にもたくさんありますが,大きな問題は上記だと思います.
そこで,本を読んだ上で以下のように修正してみました.
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")
}
}
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)
}
}
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に書くべきなのかなと思っています.
もっと勉強していきます!!最後まで読んでいただき,ありがとうございました!!