Swiftでいい感じのViewModelを作るためのメモ

フォームに入力してボタンを押すと成功や失敗が起きるというiOS GUIから、RxSwiftを使ってデザインしたViewController用のバインディングクラスがどう振る舞うといいのか考えた

![cap](https://cloud.githubusercontent.com/assets/39830/24770247/38ecbaa0-1b44-11e7-94c6-8b371e1de844.gif)

(このapp自体はどうでもいいものだけれど)


## モチベーション

- 今まで書けてなかった部分のユニットテストを書きたい
    - 非同期ストリーム(Observable)として検査できる
    - ストリームから送られてきたデータによってUIが常に同じ結果に行き着くようにする
    - とはいえプレゼンテーションの手続き部分のふるまいは保証できないけど

## 前提となる考え

- ViewModelはUIの世界から切り離された純粋なデータ
- UIView, Storyboard, UIViewController, ViewModel の責務は全部View(プレゼンテーション)
- ViewModelはViewなのでViewのデータを定義するだけ。業務ドメインやI/Oな世界の実装とは国交を持たない
- UIViewControllerはInitialなデータをObservableにしてViewModelへ渡す
- ViewModelは不変な入力の値から自身が出力するObservableを定義する。手続的に自身のプロパティを操作しない。
- UIViewControllerはViewModelの値を購読し、ストリーム毎に1つのプレゼンテーションのロジックの手続きを実行するのみ
- UIViewController内でプレゼンテーションの為の計算は行なわない

## [ViewController\.swift](https://github.com/laiso/ios-examples/blob/master/rxswift-mvvm-with-testing/SignIn/ViewController.swift)

### viewDidLoaded

`RxCocoa.Drive` はUIの世界の住人なのでViewControllerで変換する。

```swift
viewModel = ViewModel(
    emailText: emailField.rx.text.orEmpty.asObservable(),
    passwordText: passwordField.rx.text.orEmpty.asObservable(),
    submitButtonTap: submitButton.rx.tap.asObservable()
)

viewModel.nextViewPushing.asDriver(onErrorJustReturn: ())
    .drive(onNext: pushToHome)
    .addDisposableTo(disposeBag)

viewModel.requestError.asObservable()
    .filter { $0 != nil }
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: showError(error:))
    .addDisposableTo(disposeBag)
```

### ViewModel

依存する入力値のObservableはイニシャライザの引数やtupleで差し込む。
イニシャライザ経由ではなく `PublishSubject<T>`を公開し外から `bindTo` するパターンとどちらがいいのか迷った。

```swift
let nextViewPushing: Observable<Void>
let requestError = Variable<Swift.Error?>(nil)

init(
    emailText: Observable<String> = Observable.empty(),
    passwordText: Observable<String> = Observable.empty(),
    submitButtonTap: Observable<Void> = Observable.empty(),
    signIn: SignIn = SignInImpl(httpClient: URLSession.shared)
    ) {
 
    let requestError = self.requestError
    
    let fields = Observable.combineLatest(emailText, passwordText)
    
    nextViewPushing = submitButtonTap.withLatestFrom(fields)
        .flatMapLatest { statusCode, password -> Observable<String> in
            return signIn
                .sendRequest(email: statusCode, password: password)
                .catchError { e in
                    requestError.value = e
                    return Observable.empty()
                }
        }
        .do(onNext: { debugPrint($0) })
        .map { _ in () }
}
```

`let requestError = Variable<Swift.Error?>(nil)`

はイマイチだとおもう

## I/O

HTTPClientはCocoaの世界の住人なのでViewModelに直接登場させたくない。
テストコードでHTTPアクセスの結果をスタブしたいのでプロトコルで取り扱う。

```swift
protocol SignIn {
    func sendRequest(email: String, password: String) -> Observable<String>
}

struct SignInImpl: SignIn {
    let httpClient: URLSession
    
    func sendRequest(email: String, password: String) -> Observable<String> {
        let url = URL(string: "https://httpbin.org/status/\(email)")!
        let req = URLRequest(url: url)
        return httpClient.rx.data(request: req)
            .map { String(data: $0, encoding: .utf8) ?? "" }
    }
}
```

## [SignInTests\.swift](https://github.com/laiso/ios-examples/blob/master/rxswift-mvvm-with-testing/SignInTests/SignInTests.swift)

イニシャライザで入力が完結しているので RxBlockingでテストしたいViewModelの目的の値を取り出すだけ。
時間軸のある連続的な操作を想定したテストを書くにはRxTestのTestSchedulerを使う必要が出てくるかもしれない

```swift
func testSigInSuccess() {
    let viewModel = ViewModel(
        emailText: Observable.just("200"),
        passwordText: Observable.just("pass"),
        submitButtonTap: Observable.just(),
        signIn: SignInMock()
    )
    
    let tapCount = try! viewModel.nextViewPushing.toBlocking(timeout: 1).toArray()
    XCTAssertEqual(tapCount.count, 1)
}

func testSigInFailure() {
    let viewModel = ViewModel(
        emailText: Observable.just("401"),
        passwordText: Observable.just("pass"),
        submitButtonTap: Observable.just(),
        signIn: SignInMock()
    )
    
    viewModel.nextViewPushing.subscribe().disposed(by: self.disposeBag)
    let error = viewModel.requestError.value as? NSError
    XCTAssertEqual(error, SignInMock.error)
}
```

## 今後の展開

- SwiftのProtocol ExtensionやGenericを活用して異るViewControllerに設定できるViewModelを作る(Pagination等)
- RxSwift自体の学習をして使いこなす(ストリーム合成、Hot/Cold, scheduler, subject, bind, dispose, operator)
- RxSwift以外のUIデータバインディングについて調べる。Android+RxJava,RAC, XAML, JavaScript, Elm
- FluxアーキテクチャとSwiftでの実装を参考にする
- なぜかHaskell GTK+プログラミング(FRP)に目覚める
- 詳しい人がアドバイスをくれる