## iOSアプリの設計パターンについて
iOSアプリを開発する場合、単純に実装していくと、UIViewControllerが肥大化する(ファットビューコントローラー)になりがちです。
またそういったパターンの場合、ビジネスロジックに対してユニットテストが実施しにくくなりがちです。
そういった場合に、MVPなどの設計パターンを適応すると、テストしやすいアプリを作成することができます。
## MVPパターンについて
MVPパターンはModel, View, Presenterの3つのクラスで画面を構成するパターンです。
| 項目 | 説明 |
| ----- | ----- |
| Model | データ, Bluetooth通信などのロジック |
| View | ViewController, UITextFleidなどのUIController |
| Presenter | ModelとViewをつなぐ |
ModelとViewの間にPresenterを置くことで、Modelをテストしやすくします。
また、protocolを用いることでPresenterもテスト可能にします。
### メリット
- ModelとPresenterがテスト可能になる
- ViewControllerが肥大化しない
### デメリット
- Presenterが肥大化する場合がある
- ModelとViewの手続きを全てPresenterを経由するので、簡単なアプリではオーバーワークになる
## サンプルコード
ルールとして、ModelとViewの間は必ずPresenterが仲介します。
サンプルとして、何らかのTaskを追加する画面AddTaskViewControllerを題材にします。
AddTaskViewControllerでは、Taskのタイトル、期限を入力すると追加ボタンが押せるように制御します。
### Presenter
```swift
`gutter:true;
// AddTaskの入力
protocol AddTaskPresenterInput {
/// Viewが表示された
func onViewDidLoad()
/// 追加ボタンが押された
func onSelectAddTask()
/// タスクタイトルが変更された(入力チェック)
func taskTitleShouldChange(_ text: inout String)
/// タスクタイトルが変更された
func taskTitleDidChange(_ text: String)
/// 期限が変更された
func dedlineDidChange(_ deadline: Date)
}
// AddTaskの変更通知
protocol AddTaskPresenterOutput: AnyObject {
/// 追加ボタンの有効, 無効を切り替える
func updateAddButton(_ enable: Bool)
/// Taskの表示を更新する
func updateTask(_ task: Task)
// ダイアログを出す, 消す, エラー表示する
func showDialog(_ message: String)
func closeDialog()
}
final class AddTaskPresenter : AddTaskPresenterInput {
private weak var view: AddTaskPresenterOutput!
var model: AddTaskModelInput
init(view: AddTaskPresenterOutput, model: AddTaskModelInput) {
self.view = view
self.model = model
}
func onViewDidLoad() {
view.updateAddButton(false)
view.updateTask(model.task)
}
func onSelectAddTask() {
// TODO: TaskListに追加する
model.addTask(model.task)
}
func taskTitleShouldChange(_ text: inout String) {
}
func taskTitleDidChange(_ text: String) {
model.task.title = text
view.updateTask(model.task)
view.updateAddButton(isAddButtonEnable())
}
func dedlineDidChange(_ deadline: Date) {
model.task.deadline = deadline
view.updateTask(model.task)
view.updateAddButton(isAddButtonEnable())
}
}
```
Presenterでは、Viewからの入力を受け取るAddTaskPresenterInputプロトコルの実装と、viewに対する変更通知のAddTaskPresenterOutputを定義しています。
protocolとすることで、単体テストの際に自分でMockを作成してテストすることが可能になります。
### ViewController
```swift
`gutter:true;
class AddTaskViewController: UIViewController {
@IBOutlet weak var titleTextField_: EZUTextField!
@IBOutlet weak var deadLineDatePicker_: UIDatePicker!
var addButton: UIBarButtonItem = UIBarButtonItem()
private var presender: AddTaskPresenterInput!
func inject(_ presender: AddTaskPresenterInput) {
self.presender = presender
}
override func viewDidLoad() {
super.viewDidLoad()
addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(onSelectAddTask))
self.navigationItem.rightBarButtonItem = addButton
// コントロールの値変更のアクションを設定する
titleTextField_.onChangeValue = { [weak self] text in
self?.presender.taskTitleDidChange(text)
}
deadLineDatePicker_.addTarget(self, action: #selector(deadlineDidChange(_:)), for: UIControl.Event.valueChanged)
presender.onViewDidLoad()
}
@objc func onSelectAddTask() {
presender.onSelectAddTask()
}
@objc func titleFiedlDidChange(_ textField: UITextField) {
let text = textField.text ?? ""
presender.taskTitleDidChange(text)
}
@objc func deadlineDidChange(_ picker: UIDatePicker) {
presender.dedlineDidChange(picker.date)
}
}
extension AddTaskViewController: AddTaskPresenterOutput {
func updateAddButton(_ enable: Bool) {
addButton.isEnabled = enable
}
func updateTask(_ task: Task) {
titleTextField_.text = task.title
deadLineDatePicker_.date = task.deadline
}
func showDialog(_ message: String) {
}
func closeDialog() {
}
}
```
ViewControllerはpresenterに対する参照をもち、Viewのボタンが押されたり、値が変更された時にPresenterに通知します。
また、Presenterからの通知によってViewを更新する処理を実装します。
TextFieldの変更通知を受け取って、入力制限などもこのクラスでおこないます。
### Model
```swift
`gutter:true;
protocol AddTaskModelInput {
/// Taskを永続化する
func addTask(_ task: Task)
/// 設定するタスク
var task: Task { set get }
}
struct AddTaskModel: AddTaskModelInput {
var task: Task
func addTask(_ task: Task) {
// TODO: TaskListに永続化します
}
}
```
Modelもプロトコルとすることで、PresenterなどのテストでMockを使う事が可能になります。
## 最後に
MVPパターンは冗長になる事もありますが、アプリ作成の場合に活用できるパターンだと思います。
今回のサンプルコードは[Github](https://github.com/k28/ios_design_pattern/tree/main/MVPSample)をご覧ください。
必要に応じて、アラートを出す処置などをプロトコルにしてBaseViewControllerなどで実装しておくと、アラート表示を共通化できます。
## 参考文献
- [ソースコード](https://github.com/k28/ios_design_pattern/tree/main/MVPSample)
- iOSアプリ設計パターン入門
0 件のコメント :
コメントを投稿