【Swift】MVVM勉強してみたPart3

#はじめに
[前回](https://qiita.com/REON/items/421c1290139257d6ae14)に続き、[iOSアプリ設計パターン入門](https://peaks.cc/books/iOS_architecture)のMVVM(第7章)を読んで学んだことを自分なりにまとめていきます！
今回は、「Notification Centerによる実装」で簡単なアプリを作ってみます。次回は同じ動作をするアプリを「RxSwift」で作ってみたいと思います！

![ezgif.com-gif-maker (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/954451/a98769ef-afb8-2359-d398-b87d3f928897.gif)

#GitHub

https://github.com/0429oonishi/MVVMNotificationCenterRxSwift

#実装

###View

```swift:NotificationCenterViewController
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

```swift:NotificationCenterViewModel
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

```swift:NotificationCenterModel
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)
            }
        }
    }
    
}
```

```swift:ModelError
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")`の値を更新できる。


#おわりに
[次回](https://qiita.com/REON/items/592be50dbade94c1abaf)
少しずつ理解できてきました！！！