はじめに
設計パターンの一つであるMVVMをアウトプットする。
また、本来であればRxSwiftなどライブラリを使用して通知や監視を行うのがオーソドックスだが、今回は学習の一環(分かりやすい)としてNotificationCenterを使用したMVVMである。
iOSにおけるMVVM
今回作るサンプルアプリについて
-
画面上にはIDとパスワードの2つを入力するテキストフィールドが存在する
- IDとパスワードの両方に少なくとも1文字以上入っていなければならない
- どちらか一方でも文字入力がない場合、画面上にエラー文字列を表示する
-
上記を踏まえて、IDとパスワードフィールドへの入力に応じてバリデーションを行い、満たされていないときはエラーを返却します。エラーの有無に応じてラベルの文字列と色が変化させる
-
イメージなどはこのサイトで見れます
ソースコード
- Model
Swift
import Foundation
//アーキテクチャを学習する際は(おそらく)Modelからコードを定義するのが良い
//Model自身は他のコンポーネントに依存しない = ViewやViewModelがなくても「ビルド可能である」ことを表しているため
//UIに関係しないドメインロジックやそのデータを持つ(MVVMはGUIのアーキテクチャに属するため、Modelの詳細な責務分けをあまりしない)
//Modelが扱う領域の具体例
//WebAPI, データベースへのアクセス, BLEデバイス制御, 会員ステータスごとの商品の割引率の計算など多岐にわたる。
//役割
//①(今回のサンプルアプリに関して言うと)文字列のバリデーションを判定する処理を担っている
//Model
enum Result<T> {
case success(T)
case failure(Error)
}
enum ModelError: Error {
case invalidId
case invalidPassword
case invalidIdAndPassword
}
protocol ModelProtocol {
func validDate(idText: String?, passwordText: String?) -> Result<Void>
}
final class Model: ModelProtocol {
func validDate(idText: String?, passwordText: String?) -> Result<Void> {
switch (idText, passwordText) {
case (.none, .none):
return .failure(ModelError.invalidIdAndPassword)
case (.none, .some):
return .failure(ModelError.invalidId)
case (.some, .none):
return .failure(ModelError.invalidPassword)
//オプショナルバインディング
case (let idText?, let passwordText?):
switch (idText.isEmpty, passwordText.isEmpty) {
case (true, true):
return .failure(ModelError.invalidIdAndPassword)
case (false, false):
return .success(())
case (true, false):
return .failure(ModelError.invalidId)
case (false, true):
return .failure(ModelError.invalidPassword)
}
}
}
}
- ViewModel
Swift
import UIKit
//Viewを持たなくともNotificationCenterでViewModelとそのロジックが独立して存在できることがポイント
//役割
//①Viewに表示するためのデータを保持する(今回のサンプルコードにはない)
//②Viewからイベントを受け取り、Modelの処理を呼び出す
//③Viewからイベントを受け取り、加工して値を更新する
//ViewModel
final class ViewModel {
let changeText = Notification.Name("changeText")
let changeColor = Notification.Name("changeColor")
//post用(監視に通知を送る側)
private let notificationCenter: NotificationCenter
private let model: ModelProtocol
init(notificationCenter: NotificationCenter, model: ModelProtocol = Model()) {
self.notificationCenter = notificationCenter
self.model = model
}
//Viewからイベントを受け取り(ViewがこのisPasswordChangeを呼ぶということ)、Modelの処理を呼び出す関数
func idPasswordChanged(id: String?, password: String?) {
//ViewからidPasswordChanged()が呼ばれて、modelの処理を呼び出している
let result = model.validDate(idText: id, passwordText: password)
//Modelから取得した値を元にデータを加工する
switch result {
//加工した結果の文字列と色はpostしてViewに通知する(それによって監視しているViewに伝わる >>> Viewの結果が更新される)
case .success:
notificationCenter.post(name: changeText, object: "OK")
notificationCenter.post(name: changeColor, object: UIColor.green)
case .failure(let error as ModelError):
notificationCenter.post(name: changeText, object: error.errorText)
case _:
fatalError("Unexpected pattern")
}
}
}
private extension ModelError {
var errorText: String {
switch self {
case .invalidIdAndPassword:
return "IDとPasswordが未入力です"
case .invalidId:
return "IDが未入力です"
case .invalidPassword:
return "Passwordが未入力です"
}
}
}
- View
Swift
import UIKit
//MVVMではViewControllerがView
//役割
//①ユーザー入力(View)からViewModelへ伝播する
//②自身(View)の状態とViewModelの状態をデータバインディングする
//③ViewModelから返されるイベント(Notificationなど)を元に描画処理を実行する
//View
class MVVMViewController: UIViewController {
@IBOutlet private weak var idTextField: UITextField!
@IBOutlet private weak var passwordTextFiled: UITextField!
@IBOutlet private weak var validationLabel: UILabel!
//addObserver用(受け取って処理を実行する側)
private let notificationCenter = NotificationCenter()
private lazy var viewModel = ViewModel(notificationCenter: notificationCenter)
override func viewDidLoad() {
super.viewDidLoad()
idTextField.addTarget(self, action: #selector(textFieldEditingChanged), for: .editingChanged)
passwordTextFiled.addTarget(self, action: #selector(textFieldEditingChanged), for: .editingChanged)
notificationCenter.addObserver(self, selector: #selector(updateValidationText), name: viewModel.changeText, object: nil)
notificationCenter.addObserver(self, selector: #selector(uodateValidationColor), name: viewModel.changeColor, object: nil)
}
}
extension MVVMViewController {
@objc func textFieldEditingChanged(sender: UITextField) {
//テキストフィールドが選択されたとき、viewがViewModelの処理を呼び出す
viewModel.idPasswordChanged(id: idTextField.text, password: passwordTextFiled.text)
}
@objc func updateValidationText(notification: Notification) {
guard let text = notification.object as? String else { return }
validationLabel.text = text
}
@objc func uodateValidationColor(notification: Notification) {
guard let color = notification.object as? UIColor else { return }
validationLabel.textColor = color
}
}
おわりに
間違い等ございましたらコメント欄にてご指摘ください。
参考記事
開発環境
- Xcode-13.4.1
- Swift version 5.7