0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

#はじめに
前回に引き続き、MVVMを勉強していこうと思います。
今回は前回のNotificationCenterで作ったアプリをRxSwiftに書き換えます。

#GitHub

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

#実装

###View

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

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

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(())
            }
        }
    }
    
}
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.validationTextvalidationLabel.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>

#おわりに
難しい、、、

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?