#はじめに
前回に続き、iOSアプリ設計パターン入門のMVVM(第7章)を読んで学んだことを自分なりにまとめていきます!
今回は、「Notification Centerによる実装」で簡単なアプリを作ってみます。次回は同じ動作をするアプリを「RxSwift」で作ってみたいと思います!
#GitHub
#実装
###View
import UIKit
final class NotificationCenterViewController: UIViewController {
@IBOutlet private weak var idTextField: UITextField!
@IBOutlet private weak var passwordTextField: UITextField!
@IBOutlet private weak var validationLabel: UILabel!
private let notificationCenter = NotificationCenter()
private lazy var viewModel = NotificationCenterViewModel(notificationCenter: notificationCenter)
override func viewDidLoad() {
super.viewDidLoad()
idTextField.addTarget(self, action: #selector(textFieldEditignChanged), for: .editingChanged)
passwordTextField.addTarget(self, action: #selector(textFieldEditignChanged), for: .editingChanged)
//自身の状態とViewModelの状態をデーターバインディングする
notificationCenter.addObserver(self, selector: #selector(updateValidationText), name: viewModel.changeText, object: nil)
//自身の状態とViewModelの状態をデーターバインディングする
notificationCenter.addObserver(self, selector: #selector(updateValidationColor), name: viewModel.changeColor, object: nil)
}
}
private extension NotificationCenterViewController {
@objc func textFieldEditignChanged(sender: UITextField) {
//ユーザーの入力をViewModelに渡す
viewModel.idPasswordChanged(id: idTextField.text, password: passwordTextField.text)
}
@objc func updateValidationText(notification: Notification) {
//ViewModelから返されるイベントを元に描画処理を実行する
guard let text = notification.object as? String else { return }
validationLabel.text = text
}
@objc func updateValidationColor(notification: Notification) {
//ViewModelから返されるイベントを元に描画処理を実行する
guard let color = notification.object as? UIColor else { return }
validationLabel.textColor = color
}
}
###ViewModel
import UIKit
final class NotificationCenterViewModel {
let changeText = Notification.Name("changeText")
let changeColor = Notification.Name("changeColor")
private let notificationCenter: NotificationCenter
private let model: NotificationCenterModelProtocol
init(notificationCenter: NotificationCenter,
model: NotificationCenterModelProtocol = NotificationCenterModel()) {
self.notificationCenter = notificationCenter
self.model = model
}
//Viewからイベントを受け取り、Modelの処理を呼び出す
func idPasswordChanged(id: String?, password: String?) {
let result = model.validate(idText: id, passwordText: password)
switch result {
case .success:
//Viewからイベントを受け取り、加工して値を更新する
notificationCenter.post(name: changeText, object: "OK!!!")
//Viewからイベントを受け取り、加工して値を更新する
notificationCenter.post(name: changeColor, object: UIColor.green)
case .failure(let error as ModelError):
//Viewからイベントを受け取り、加工して値を更新する
notificationCenter.post(name: changeText, object: error.errorText)
//Viewからイベントを受け取り、加工して値を更新する
notificationCenter.post(name: changeColor, object: UIColor.red)
case _:
fatalError("Unexpected pattern.")
}
}
}
###Model
protocol NotificationCenterModelProtocol {
func validate(idText: String?, passwordText: String?) -> Result<Void>
}
final class NotificationCenterModel: NotificationCenterModelProtocol {
func validate(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)
}
}
}
}
enum Result<T> {
case success(T)
case failure(Error)
}
enum ModelError: Error {
case invalidId
case invalidPassword
case invalidIdAndPassword
var errorText: String {
switch self {
case .invalidIdAndPassword: return "IDとPasswordが未入力です。"
case .invalidId: return "IDが未入力です。"
case .invalidPassword: return "Passwordが未入力です。"
}
}
}
#解説
###View
Viewの責務は以下の3つです。
・ユーザーの入力をViewModelに渡す
ユーザー入力から始まるプレゼンテーションロジックをViewModelの責務として実現するため、Viewからユーザー入力をViewModelに渡す。(textFieldへの入力をViewModelに渡す)
文字列が入力された時に呼ぶメソッドをそれぞれaddTargetで登録しておき、textFieldEditignChanged
でIDまたはパスワードの文字列をviewModelに伝える。
・自身の状態とViewModelの状態をデーターバインディングする
ViewModelからViewへの通知にNotificationCenterを使ってデータバインディングを行う。
ViewModelからの通知をViewが監視し、そのイベントでViewが画面を更新できるようにする。
つまり、ViewModel側で定義したNotification.Name(viewModel.changeTextとviewModel.changeColor)
がviewModelでpostされるたびに、updateValidationText(notification:)
とupdateValidationColor(notification:)
が呼び出されるようにする。
・ViewModelから返されるイベントを元に描画処理を実行する
Notification Centerで受け取った通知を使い、Viewで実際に画面を更新する。
データバインディングしたイベントで描画を実行するメソッドを定義。
###ViewModel
ViewModelの責務は以下の3つです。
・Viewに表示するためのデータを保持する
プレゼンテーションロジックの判定に使ったり、ViewModel内でデータを加工したりするために、ViewModelはViewに表示するためのデータを保持する。(今回は状態を保持する必要がないため登場しない)
・Viewからイベントを受け取り、Modelの処理を呼び出す
Viewが呼ぶidPasswordChanged(id:password)
を元に、Modelを呼び出したりModelから新たな値を取得したりする。
コードではtextFieldの文字列の入力イベントに対して、modelのvalidate(idText:passwordText:)
を呼んでバリデーション結果を取得している。
・Viewからイベントを受け取り、加工して値を更新する
Viewからイベントを受け取り、自身がもつ値を加工して表示を更新させることもViewModelの役割。
Modelから受け取った値を加工することもあればModelと関わりのない表示上のデータ加工であればModelを利用せず、ViewModelの内部で処理して描画処理を行うこともある。
コードではidPasswordChanged(id:password:)
のなかでModelから取得した値を元にデータを加工している。加工した結果の文字列と色はNotificationCenterのpost(name:object:)
によって通知する。これにより、監視しているViewControllerに伝わる。
###Model
Modelはプレゼンテーションロジック以外のドメインロジックを担当する。
今回のアプリでは、文字列のバリデーションがドメインロジック。Modelはその判定処理を担っている。
#今回追加で学んだこと
・@objc func
はextension
で分けておくと、みやすくなる
・addObserver
を通知を受け取りたい側で登録することで、Notification.Name("SomeString")
通知が発行されたタイミングでセレクタ関数が実行される。
・NotificationCenter
のpost
メソッドでViewで登録しておいたNotification.Name("SomeString")
の値を更新できる。
#おわりに
次回
少しずつ理解できてきました!!!