【Swift】MVVM勉強してみたPart4

#はじめに
[前回](https://qiita.com/REON/items/60b9fb8fc01e5cc478e5)に引き続き、MVVMを勉強していこうと思います。
今回は前回のNotificationCenterで作ったアプリをRxSwiftに書き換えます。

#GitHub

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

#RxSwift
・メリット
MVVMに不可欠なデータバインディングを備えている
非同期処理の簡易化
Result型の代替
オペレーションによるデータ変換
・デメリット
やはり学習コストが高い

#実装

###View

```swift:RxSwiftViewController
import UIKit
import RxSwift
import RxCocoa

final class RxSwiftViewController: UIViewController {

    @IBOutlet private weak var idTextField: UITextField!
    @IBOutlet private weak var passwordTextField: UITextField!
    @IBOutlet private weak var validationLabel: UILabel!
                                 //ユーザーの入力をViewModelに伝える
    private lazy var viewModel = RxSwiftViewModel(idTextObservable: idTextField.rx.text.asObservable(),
                                                  passwordTextObservable: passwordTextField.rx.text.asObservable(),
                                                  model: RxSwiftModel())
    private let disposeBag = DisposeBag()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //自身の状態とViewModelの状態をデータバインディング
        viewModel.validationText
            .bind(to: validationLabel.rx.text)
            .disposed(by: disposeBag)
        
        viewModel.loadLabelColor
            .bind(to: loadLabelColor)
            .disposed(by: disposeBag)

    }
    
    //ViewModelから返されるイベントを元に画面処理を実行
    private var loadLabelColor: Binder<UIColor> {
        return Binder(self) { me, color in
            me.validationLabel.textColor = color
        }
    }

}
```

###ViewModel

```swift:RxSwiftViewModel
import RxSwift
import RxCocoa

final class RxSwiftViewModel {
    
    let validationText: Observable<String>
    let loadLabelColor: Observable<UIColor>
    
    init(idTextObservable: Observable<String?>,
         passwordTextObservable: Observable<String?>,
         model: RxSwiftModelProtocol) {
        
        //Viewからイベントを受け取り、Modelの処理を呼ぶ
        let event = Observable
            .combineLatest(idTextObservable, passwordTextObservable)
            .skip(1)
            .flatMap { idText, passwordText -> Observable<Event<Void>> in
                return model
                    .validate(idText: idText, passwordText: passwordText)
                    .materialize()
            }
            .share()
        
        
        //Viewからイベントを受け取り、加工して値を更新する
        self.validationText = event
            .flatMap { event -> Observable<String> in
                switch event {
                case .next:
                    return .just("OK!!!")
                case let .error(error as ModelError):
                    return .just(error.errorText)
                case .error, .completed:
                    return .empty()
                }
            }
            .startWith("IDとPasswordを入力してください。")
        
        //Viewからイベントを受け取り、加工して値を更新する
        self.loadLabelColor = event
            .flatMap { event -> Observable<UIColor> in
                switch event {
                case .next:
                    return .just(.green)
                case .error:
                    return .just(.red)
                case .completed:
                    return .empty()
                }
            }
        
    }
    
}
```

###Model

```swift:RxSwiftModel
import RxSwift
import RxCocoa

//protocol化して疎結合に、テスタブルにする
protocol RxSwiftModelProtocol {
    func validate(idText: String?, passwordText: String?) -> Observable<Void>
}

final class RxSwiftModel: RxSwiftModelProtocol {
    
    //Observableを返却してViewModelと合成しやすくする
    func validate(idText: String?, passwordText: String?) -> Observable<Void> {
        switch (idText, passwordText) {
        case (.none, .none):
            return Observable.error(ModelError.invalidIdAndPassword)
        case (.none, .some):
            return Observable.error(ModelError.invalidId)
        case (.some, .none):
            return Observable.error(ModelError.invalidPassword)
        case (let idText?, let passwordText?):
            switch (idText.isEmpty, passwordText.isEmpty) {
            case (true, true):
                return Observable.error(ModelError.invalidIdAndPassword)
            case (true, false):
                return Observable.error(ModelError.invalidId)
            case (false, true):
                return Observable.error(ModelError.invalidPassword)
            case (false, false):
                return Observable.just(())
            }
        }
    }
    
}
```

```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

・ユーザーの入力をViewModelに伝える
RxではViewModelを作成するときにIDとパスワード入力フィールドのObservableを渡す。
`textField.rx.text.asObservable()`でUITextFieldがもつrx拡張からtextプロパティを取り出し、asObservable()によってObservableを取り出している。

・自身の状態とViewModelの状態をデータバインディング
ViewModelが公開するObservableのプロパティはViewModel内のプレゼンテーションロジックによって値が更新される。ViewではそのObservableとViewで同期させたいデータとをバインディングしてデータの更新をUIに反映できる。
今回の例では`viewModel.validationText`と`validationLabel.rx.text`とを`bind(to:)`によってデータバインディングしている。こうすることでvalidationTextが変更されるのに同期してvalidationLabelの文字列も更新される。

・ViewModelから返されるイベントを元に画面処理を実行
UILabelのtextColorのようにrx拡張がないプロパティを更新する場合にはObservableを独自に定義する必要がある。
今回の例では色の更新処理をBinder化して`bind(to:)`によってviewModelのloadLabelColorプロパティ（Observable<UIColor>型）とデータバインディングしている。

###ViewModel

・Viewに表示するためのデータを保持する
プレゼンテーションロジックの判定に使ったり、ViewModel内でデータを適宜加工したりするためにViewModelはViewに表示するためのデータを保持する。
今回の例ではデータを保持する必要はないため、実装にはありません。
もし必要な場合は`BehaviorRelay`を使ってデータを保持するのがいい。

・Viewからイベントを受け取り、Modelの処理を呼ぶ
Viewからのイベントを元にしてModelを呼び出したり、Modelから新たな値を取得する。
その結果によっては自身がもつデータを更新し、データバインディングによってViewを更新する。
今回の例では、ViewModelのイニシャライザでtextFieldの文字列の入力、変更イベントに同期してModelの`validate(idText:passwordText:)`を呼び出すように関連づけしている。
IDとパスワードをそれぞれで変更があったときに共通の処理を呼び出すので、`combineLatest()`を使って二つの入力（Observable）を合成している。
`materialize()`によってonNext, onError, onCompleteのイベントを`Obsevable<Event<Void>>`として変換し、それぞれ別々のストリームとして扱えるようにしている。
`share()`でHotObservableに変換し、一つの入力に対してこれ以降のObservableがそれぞれ独立したストリームとしてデータ更新を行えるようにしている。
その結果をeventプロパティで保持して以降の処理で利用する。

・Viewからイベントを受け取り、加工して値を更新する
Modelから取得した値を元にデータ加工を行う。
eventを入力として`flatmap(_:)`に入力されるイベントに応じてViewに出力する情報を加工し、更新する。
validationTextとloadLabelColorのそれぞれとデータバインディングしたViewがこの情報を受け取って直ちに表示を更新する。
ViewModelはユーザー入力に反応してデータ加工やModelの呼び出しを行う。そして、プレゼンテーションロジックに応じて公開しているObservableにイベントを入力することでそれらとデータバインディングしているViewへの描画指示を行う。

###Model
・protocol化して疎結合に、テスタブルにする
ViewModelからはModelに直接依存するのではなく、Modelのprotocolに依存するようにすることで、[DI](https://qiita.com/kusumotoa/items/c924d5c67282fd11f664#dependency-injection%E3%81%A8%E3%81%AF)ができるようになる。疎結合になり、テスタブルにもなる。

・Observableを返却してViewModelと合成しやすくする
ViewModelからModelを扱う場合、Modelの処理結果をObservableで返却することで利便性が高くなる。(ViewModelがもつObservableと合成しやすいため)
Observableを返却してViewModelとの建て付けをよくする。
`func validate(idText: String?, passwordText: String?) -> Observable<Void>`

#おわりに
難しい、、、