#はじめに
前回に引き続き、MVVMを勉強していこうと思います。
今回は前回のNotificationCenterで作ったアプリをRxSwiftに書き換えます。
#GitHub
#RxSwift
・メリット
MVVMに不可欠なデータバインディングを備えている
非同期処理の簡易化
Result型の代替
オペレーションによるデータ変換
・デメリット
やはり学習コストが高い
#実装
###View
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
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
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(())
}
}
}
}
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型)とデータバインディングしている。
###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ができるようになる。疎結合になり、テスタブルにもなる。
・Observableを返却してViewModelと合成しやすくする
ViewModelからModelを扱う場合、Modelの処理結果をObservableで返却することで利便性が高くなる。(ViewModelがもつObservableと合成しやすいため)
Observableを返却してViewModelとの建て付けをよくする。
func validate(idText: String?, passwordText: String?) -> Observable<Void>
#おわりに
難しい、、、