RxSwiftでGitHubのリポジトリを検索するサンプル

RxSwiftでMVVMパターンのアプリを作ろうとしましたが、
実際に動いていて、参考にできるサンプルがあまりなかったので、
手探りで作ったサンプルを残しておきます。

ソースコードはこちらにあります。
[akaimo/RxSwift-GitHubRepository-Search](https://github.com/akaimo/RxSwift-GitHubRepository-Search)

# 環境
- Swift 2.2
- Xcode 7.3

## RxSwiftのセットアップ
投稿時の最新バージョンである、`2.3.1`で動作確認をしております。
RxSwiftはアップデートが頻繁にあるため、これ以降のバージョンだと動かないおそれがあります。
動かしてみるときは、`carthage bootstrap --platform iOS`でインストールしてください。

# サンプルアプリ
このサンプルはテキストフィールドに入力されたテキストで、GitHubからリポジトリを検索するだけの単純なものです。

## 機能１： バリデーション
ただ検索するだけではつまらないと思ったので、簡単な文字数制限をつけてみました。
文字数が足りないときはエラーメッセージが表示され、検索ボタンが押せなくなります。

バリデーションで制御されるのはこの3つです。

```swift:
@IBOutlet weak var searchButton: UIButton!
@IBOutlet weak var searchTextField: UITextField!
@IBOutlet weak var validMessage: UILabel!
```

基本的に処理はviewModelでやっていきたいため、テキストとタップとリターンを送ります。

```swift:SearchViewController.swift       
        viewModel = SearchViewModel(
            search: searchTextField.rx_text.asObservable(),
            buttonTap: searchButton.rx_tap.asObservable(),
            keyboardReturn: searchTextField.rx_controlEvent(.EditingDidEndOnExit).asObservable()
        )
```

viewModelでバリデーションの処理をします。

```swift:SerchViewModel.swift
        let minimumSize = 3
        validation = search
            .map { $0.characters.count >= minimumSize }
            .shareReplay(1)
```

バリデーション結果をviewとバインディングすれば完成です。

```swift:SearchViewController.swift
        viewModel.validation
            .bindTo(validMessage.rx_hidden)
            .addDisposableTo(disposeBag)
        
        viewModel.validation
            .bindTo(searchButton.rx_enabled)
            .addDisposableTo(disposeBag)

        searchTextField.rx_text
            .bindTo(viewModel.searchText)
            .addDisposableTo(disposeBag)
```
ついでに検索で使うテキストもviewModelとバインディングしておきます。
以上でバリデーションは完成です。

## 機能2: リポジトリ検索
今回のメイン機能です。
APIKitとHimotokiを使ってGitHubのAPIをたたきます。

まず、APIKitをRxSwiftで使えるようにします。

```swift:Session+Rx.swift
import RxSwift
import APIKit

extension Session {
    static func rx_response<T: RequestType>(request: T) -> Observable<T.Response> {
        return Observable.create { observer in
            let task = sendRequest(request) { result in
                switch result {
                case .Success(let response):
                    observer.on(.Next(response))
                    observer.on(.Completed)

                case .Failure(let error):
                    observer.onError(error)
                }
            }

            return AnonymousDisposable {
                task?.cancel()
            }
        }
    }
}
```

次に、検索ボタンのタップ、キーボードのリターン、どちらかのアクションをトリガーにAPIをたたきます。

```swift:SearchViewModel.swift
        request = Observable
            .of(buttonTap, keyboardReturn)
            .merge()
            .withLatestFrom(searchText.asObservable())
            .filter { $0.characters.count >= minimumSize }
            .map { SearchRepositoriesRequest(query: $0) }
            .shareReplay(1)
        
        response = request
            .flatMap { request in
                return Session
                    .rx_response(request)
                    .doOnError { PublishSubject<ErrorType>().onNext($0) }
                    .catchError { _ in Observable.empty() }
            }
            .shareReplay(1)
```
この部分にはAPIKitが関わってくるので、よくわからない場合はAPIKitのドキュメントを読んでみることをオススメします。

最後にレスポンス結果をviewModelに格納すれば完了です。

```swift:SearchViewModel.swift
        response
            .map { $0.repository }
            .bindTo(repositories)
            .addDisposableTo(disposeBag)
        
        repositories
            .asObservable()
            .subscribeNext { print($0) }
            .addDisposableTo(disposeBag)
```
このままだと検索結果がみれないので、まずは標準出力にだしておきます。

### 通信中の処理と画面遷移

もう少しまともなサンプルにするために、検索結果を次の画面に表示させます。
ボタンを押してから画面遷移までに通信のタイムラグがあるので、インジケータを表示させます。

さきほどの`request`と`response`に反応して出し入れします。

```swift:SearchViewController.swift
        viewModel.request
            .subscribeNext { [weak self] _ in
                MBProgressHUD.showHUDAddedTo(self?.view, animated: true)
            }
            .addDisposableTo(disposeBag)

        viewModel.response
            .subscribeNext { [weak self] response in
                MBProgressHUD.hideHUDForView(self?.view, animated: true)
                self?.view.endEditing(true)
                
                let vc = self?.storyboard?.instantiateViewControllerWithIdentifier("ResultViewController") as! ResultViewController
                vc.title = self?.searchTextField.text
                vc.viewModel.repository.value = response.repository
                self?.navigationController?.pushViewController(vc, animated: true)
            }
            .addDisposableTo(disposeBag)
```
インジケータの消滅と同時に次ぎの画面に遷移させ、tableViewに表示させたら今回のサンプルは完成です。

```swift:ResultViewController.swift
        viewModel.repository
            .asObservable()
            .bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) {  _, repository, cell in
                cell.textLabel?.text = repository.fullName
            }
            .addDisposableTo(disposeBag)
```

# まとめ
私がRxSwiftへの入門に大変苦労しているため、次に学ぶ方の資料となればと思い書きました。
Rxにまだなれていないので、おかしなところがあると思いますが、優しく教えてもらえると幸いです。

## 参考
- https://github.com/ReactiveX/RxSwift
- https://github.com/ishkawa/sandbox/tree/master/RxPagination