MVVMをさらっとアウトプットしてみた（NotificationCenter編）

## はじめに
設計パターンの一つであるMVVMをアウトプットする。
また、本来であればRxSwiftなどライブラリを使用して通知や監視を行うのがオーソドックスだが、今回は学習の一環（分かりやすい）としてNotificationCenterを使用したMVVMである。

## iOSにおけるMVVM
- [こちらのサイトの図](https://engineering.mercari.com/en/blog/entry/2019-06-12-120000/)が分かりやすいです
![スクリーンショット 2023-02-19 22.06.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2915639/8d02bde2-f9d2-68a9-bd01-39344e515fae.png)

- MVVMの各々の役割等はソースコードと同時に記載している
    - [下記ソースコード](https://qiita.com/RammaruIshii/items/3c956b5b5af81fdea626#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89)を参照ください。



## 今回作るサンプルアプリについて
- 画面上にはIDとパスワードの２つを入力するテキストフィールドが存在する
    - IDとパスワードの両方に少なくとも１文字以上入っていなければならない
    - どちらか一方でも文字入力がない場合、画面上にエラー文字列を表示する

- 上記を踏まえて、IDとパスワードフィールドへの入力に応じてバリデーションを行い、満たされていないときはエラーを返却します。エラーの有無に応じてラベルの文字列と色が変化させる

- イメージなどは[このサイト](https://peaks-pdfs2.s3-ap-northeast-1.amazonaws.com/iOS_architecture_book_sample.pdf)で見れます
![スクリーンショット 2023-02-19 22.18.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2915639/9afae17e-4269-fb85-6639-ce64e9e29a2a.png)

## ソースコード
- Model
```swift: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: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: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
    }
}
```
## おわりに
間違い等ございましたらコメント欄にてご指摘ください。

## 参考記事
- [iOSアプリ設計パターン入門](https://peaks.cc/books/iOS_architecture)
- [iOSアプリ設計パターン入門の公開PDF](https://peaks-pdfs2.s3-ap-northeast-1.amazonaws.com/iOS_architecture_book_sample.pdf)
- [mercari engineering
Mercari Engineering Portal ](https://engineering.mercari.com/en/blog/entry/2019-06-12-120000/)

## 開発環境
- Xcode-13.4.1
- Swift version 5.7