RxSwiftでMVVMパターンのアプリを作ろうとしましたが、
実際に動いていて、参考にできるサンプルがあまりなかったので、
手探りで作ったサンプルを残しておきます。
ソースコードはこちらにあります。
akaimo/RxSwift-GitHubRepository-Search
環境
- Swift 2.2
- Xcode 7.3
RxSwiftのセットアップ
投稿時の最新バージョンである、2.3.1
で動作確認をしております。
RxSwiftはアップデートが頻繁にあるため、これ以降のバージョンだと動かないおそれがあります。
動かしてみるときは、carthage bootstrap --platform iOS
でインストールしてください。
サンプルアプリ
このサンプルはテキストフィールドに入力されたテキストで、GitHubからリポジトリを検索するだけの単純なものです。
機能1: バリデーション
ただ検索するだけではつまらないと思ったので、簡単な文字数制限をつけてみました。
文字数が足りないときはエラーメッセージが表示され、検索ボタンが押せなくなります。
バリデーションで制御されるのはこの3つです。
@IBOutlet weak var searchButton: UIButton!
@IBOutlet weak var searchTextField: UITextField!
@IBOutlet weak var validMessage: UILabel!
基本的に処理はviewModelでやっていきたいため、テキストとタップとリターンを送ります。
viewModel = SearchViewModel(
search: searchTextField.rx_text.asObservable(),
buttonTap: searchButton.rx_tap.asObservable(),
keyboardReturn: searchTextField.rx_controlEvent(.EditingDidEndOnExit).asObservable()
)
viewModelでバリデーションの処理をします。
let minimumSize = 3
validation = search
.map { $0.characters.count >= minimumSize }
.shareReplay(1)
バリデーション結果をviewとバインディングすれば完成です。
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で使えるようにします。
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をたたきます。
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に格納すれば完了です。
response
.map { $0.repository }
.bindTo(repositories)
.addDisposableTo(disposeBag)
repositories
.asObservable()
.subscribeNext { print($0) }
.addDisposableTo(disposeBag)
このままだと検索結果がみれないので、まずは標準出力にだしておきます。
通信中の処理と画面遷移
もう少しまともなサンプルにするために、検索結果を次の画面に表示させます。
ボタンを押してから画面遷移までに通信のタイムラグがあるので、インジケータを表示させます。
さきほどのrequest
とresponse
に反応して出し入れします。
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に表示させたら今回のサンプルは完成です。
viewModel.repository
.asObservable()
.bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) { _, repository, cell in
cell.textLabel?.text = repository.fullName
}
.addDisposableTo(disposeBag)
まとめ
私がRxSwiftへの入門に大変苦労しているため、次に学ぶ方の資料となればと思い書きました。
Rxにまだなれていないので、おかしなところがあると思いますが、優しく教えてもらえると幸いです。