はじめに
今回はMVPを簡単なサンプルとともに紹介していきます。
MVPのイメージを記事にしたものもあるのでそちらもよろしければどうぞ。
想定読者
- MVP聞いたことあるけどよく知らないなという人
- MVCで開発しているけどかなりコードの見通しが悪くなってきてどうにかしたいという人
サンプルアプリ
今回MVPを紹介するために作るアプリは単純な割り算アプリです。
機能詳細
- 割られる数と割る数を入力すると計算結果が表示されます
- 割る数が0の場合はエラーを表示します
※入力された文字が数字じゃなかったらなどなど細かいところはかなり適当に実装しております
Model、View、Presenterのそれぞれの役割
Presenter
Presenterの仕事
-
Viewからイベントを受け取る
- Viewが読み込まれたとか
- ボタンがタップされたとか
-
Viewからのイベントを受けて、Modelに処理を依頼する
- サーバーから値を取得するとか
- emailのバリデーションとか
-
Modelから返ってきた処理の結果をもとに、Viewに何を表示させるか判断、指示をする
- 計算結果を表示させるとか
- エラーを表示させるとか
Presenterの特徴
-
import UIKit
はしない。 -
以下のような具体的な表示方法は知りません。(
View
の仕事)- CollectionViewで表示する?
- Labelで表示する?
- アラートで表示する?
-
以下のような具体的な処理方法は知りません。(
Model
の仕事)- サーバーから値を取ってくる?
- UserDefaultから値を取ってくる?
以上のようにPresenterは詳細は知りません。ただ、処理の流れは知っています。
つまり、仕様書のような働きをしてくれます。いつどんなことをするか?だれにそれをお願いするのか?みたいなことが一覧で見れるイメージ。
Presenterを見ると、その画面がどのような処理の流れになっているかが分かりやすくなります。
逆を言えば、Presenterをみたら流れが分かるように処理を書いて、命名していけばいいということですね。
Presenterのコード
Presenterのコードです。
MVPで実装するようになって、Presenterを見れば大体の処理の流れがわかるまでの時間が圧倒的に速くなりました。
import Foundation
// InputにはViewControllerからどんなイベントを受け取るかを書きます
// 今回のサンプルではボタンをタップされたときに値を受け取って、Modelに渡したい
// Inputの命名はViewでどんなイベントが起こったかを関数名にすると分かりやすくなります
// 例)画面が読み込まれた時に何かの処理が必要ならViewDidLoadというメソッドを作るといいでしょう
protocol PresenterInput: AnyObject {
func tappedButton(dividend: Int, divisor: Int)
// 例)func viewDidLoad()
}
// OutputにはModelから処理の結果を受け取ったのち、何を表示してほしいかを書きます
// 今回は計算結果を表示するメソッド、割る数が0でエラーを表示するメソッドを実装しています
protocol PresenterOutput: AnyObject {
func showResult(quotient: Int)
func showError(error: DivisionError)
}
final class Presenter {
weak var view: PresenterOutput?
private let model: DivisionModelInput
// このへん何をしているんだろうという人は「依存性注入(DI)」を調べてみてください。
init(view: PresenterOutput, model: DivisionModelInput) {
self.view = view
self.model = model
}
}
extension Presenter: PresenterInput {
// ButtonをタップされたときにModelに処理を依頼します
// 返ってきた値を元にViewに何を表示してほしいかを指示します
func tappedButton(dividend: Int, divisor: Int) {
model.fetchQuotient(dividend: dividend, divisor: divisor) { [weak self] result in
guard let strongSelf = self else { return }
switch result {
case .success(let quotient):
strongSelf.view?.showResult(quotient: quotient)
case .failure(let error):
strongSelf.view?.showError(error: error)
}
}
}
}
View
Viewの役割
- Viewで発生したイベントをPresenterに伝えます
- 表示しろとPresenterから言われたものを表示します
- どのようなUIパーツを使って表示するか決めます
Viewの特徴
-
import UIKit
をします - Presenterにイベントを通知した後どんな処理をしているのかは知りません
- Viewは何を出そうか?みたいなことは判断しません
- とにかくPresenterから言われたものを表示します
- Modelと直接やりとりはしません
サンプルコード
import UIKit
final class ViewController: UIViewController {
@IBOutlet private weak var dividendTextField: UITextField!
@IBOutlet private weak var divisorTextField: UITextField!
@IBOutlet private weak var resultLabel: UILabel!
@IBAction private func tappedButton(_ sender: UIButton) {
let dividend = Int(dividendTextField.text!)!
let divisor = Int(divisorTextField.text!)!
presenter.tappedButton(dividend: dividend, divisor: divisor)
}
private var presenter: PresenterInput!
override func viewDidLoad() {
super.viewDidLoad()
presenter = Presenter(view: self, model: DivisionModel())
}
}
extension ViewController: PresenterOutput {
func showResult(quotient: Int) {
resultLabel.text = String(quotient)
}
func showError(error: DivisionError) {
switch error {
case .dividendByZero:
let alert = UIAlertController(title: "エラー", message: "0では割れません", preferredStyle: .alert)
let ok = UIAlertAction(title: "はい", style: .default, handler: nil)
alert.addAction(ok)
present(alert, animated: true, completion: nil)
}
}
}
Model
Modelの役割
- Presenterから依頼された処理を行い、結果を返却します
- PresenterとView以外の役割を担います
- サーバーと通信したり
- パスワードを設定機能で使うような文字のバリデーションをおこなったり
Modelの特徴
-
import UIKit
はしません - PresenterとView以外の全てなので、かなりカバー範囲は広くなります
- Presenterとやりとりします
- Viewとの直接的なやりとりはありません
サンプルコード
Modelに関しては特にひねりもなくといった感じでしょうか。
サーバーで処理をして受け取るという設定にしたので、クロージャでResultを返すようにしてみました。
import Foundation
enum DivisionError: Error {
case dividendByZero
}
protocol DivisionModelInput {
func fetchQuotient(dividend: Int, divisor: Int, completion: @escaping (Result<Int, DivisionError>) -> Void)
}
final class DivisionModel: DivisionModelInput {
// サーバーと通信して値を取得するということを想定してクロージャを使用しました。
func fetchQuotient(dividend: Int, divisor: Int, completion: @escaping (Result<Int, DivisionError>) -> Void) {
if divisor == 0 {
completion(.failure(.dividendByZero))
return
}
let quotient = dividend / divisor
completion(.success(quotient))
}
}
プラスアルファ
MVPの課題
MVPで開発を進めていくとModelの部分をどのように扱うかということが大きな壁になります。そこをどうしていくかは、クリーンアーキテクチャやVIPERなどといったシステムアーキテクチャを学んでいく必要があります。ちなみにMVPやMVC、MVVMはGUIアーキテクチャと呼ばれています。
MVPにおけるProtocol
PresenterでInput、Outputという2つのProtocolを使用しました。
他の記事を見ていても、このように書かれていることが多いと思います。ただ、Protocolが必ずPresenterには必要かというとそうではないと思っています。大事なのはPresentationロジックをViewControllerから切り離すことだと思います。
おわりに
いかがっだったでしょうか?MVPの理解に少しでも近づけていたら嬉しいです。
また、僕の認識が間違っているところがあれば、ぜひ教えていただけたら嬉しいです!
それでは!