2016/04/02更新:RxSwiftでUIの更新にはDriverを使ってみた記事も書いてみました!
RxSwift
Reactive ProgrammingをSwiftで実現するライブラリ
Qiitaの記事も増えてきました!
便利そうだけど使いどころは・・・?
ということで、Swiftビギナーな自分がRxSwiftでMVVMモデルなRSSFeedアプリを作ってみたまとめ
どんなアプリ?
環境
- Xcode 7.2
- Swift 2.1
- RxSwift 2.0.0
- iOS8以上
リポジトリ
- Sketch App Sourcesで配信しているRSS FeedをTableViewでリスト表示
- リストを選択すると詳細をWebViewで表示
- 取得するRSSはXMLなので、Google AJAX Feed APIを使ってJSONで取得
TIPS: 今回は練習用アプリなのでATSは無効化してあります
##使用ライブラリ
Library | Use |
---|---|
Alamofire | Swift用通信ライブラリ |
ObjectMapper | JSONのレスポンスをFeedクラスにマッピング |
HTMLReader | JSONに混在している HTMLタグをパース |
RxSwift | 今回の主役、ReactiveProgrammingをサポート |
SDWebImage | (Obj-C製)リストの画像ロードに使用 |
SVProgressHUD | (Obj-C製)詳細画面のローディング表示 |
当初、めちゃいけてるPullRefreshライブラリDGElasticPullToRefreshを使う予定でしたが、AutoLayoutのバグで使用を断念
RxSwiftでのMVVMモデルの実現
こんなイメージ
RxSwiftの使い所
1. Model : リクエストでObservable
オブジェクトの生成
ここでは、APIリクエストでObservable
オブジェクトを生成
レスポンスのJSONは、ObjectMapperを利用して作成したモデルクラスFeedResponse
にマッピングしておくと楽でした
func connect() -> Observable<FeedResponse> {
let observable: Observable<FeedResponse> = Observable.create { (observer: AnyObserver<FeedResponse>) in
self.request?.responseJSON(completionHandler: { response in
print("\(response)")
switch response.result {
case .Success(let value):
guard let object = Mapper<FeedResponse>().map(value) else {
return observer.onCompleted()
}
observer.onNext(object)
observer.onCompleted()
case .Failure(let error):
observer.onError(error)
}
})
return AnonymousDisposable {
}
}
return observable
}
TIPS: func connect() -> Observable<FeedResponse>
の部分をジェネリクスにすると、他のモデルクラスでも汎用的に使えます
ちなみに、この実装を実現しているライブラリがtaktem/TAKSwiftSupportです
こちらのRequestBase
を親としたリクエストクラスを作成すると便利です
taktemさんありがとうございます
TIPS: Observable.create
はRxSwift 2.0.0-beta以前ではcreate
のみの記述でした
2. ViewModel : リクエスト呼び出し、レスポンスをVariable
変数に格納
プロパティにはVariable
型の変数を宣言しておく
このとき、Variable
変数は初期化の際にも値の更新とみなされsendNext
されてしまうので、意図しないSubscribe
を避けるためfilteringできるように空もしくはnilにしておきます
/// Request class
private var request = FeedRequest()
/// Feed
private(set) var feed: Variable<Feed?> = Variable(nil)
/// Entry
private(set) var entries: Variable<[Entry]> = Variable([])
/// Error
private(set) var error: Variable<NSError?> = Variable(nil)
// 省略
func reloadData() {
request.connect().subscribe(onNext: { [weak self] x in
// Variableプロパティに格納
self?.entries.value = x.feed.entries
}, onError: { error in
}, onCompleted: { () in
}) { () in
}.addDisposableTo(request.disposeBag)
}
リクエストから受け取ったFeedResponse
型のオブジェクトx
から、TableViewに表示するデータソース用の配列entries
オブジェクトを取り出し、プロパティに代入し直す
この代入をトリガーにして、TableViewのリロードを行います
TIPS: Variable
の変数は外部クラスから書き換えを禁止するためアクセスレベルをprivate(set)
にしておくと安全です
3. ViewController : ViewModelのVariable
変数をSubscribe
ViewModelで宣言したVariable
オブジェクトは.asObservable()
でObservable
に変換できるので、ViewController上でSubscribe
を行います
2.で代入した際の値の更新をトリガーに、TableViewのリロードを実施
func bind() {
// Connection
viewModel.entries.asObservable().filter { x in
// 初期化の際にsubscribeをさけるためfilter
// 配列の要素数が0件より多くなったときにsubscribeを実施
return !x.isEmpty
}.subscribe(onNext: { [unowned self] x in
// 更新
self.refresh.endRefreshing()
self.tableView.reloadData()
}, onError: { error in
// エラー
// FIXME: いつかやるかも
}, onCompleted: { () in
// 完了
}) { () in
}.addDisposableTo(disposeBag)
// Pull Refresh
refresh.rx_controlEvent(.ValueChanged).subscribeNext { [unowned self] x -> Void in
// プルリフレッシュを購読し、ValueChangedがあった場合APIリロードを実行
self.viewModel.reloadData()
}.addDisposableTo(disposeBag)
}
TIPS: .filter
はViewModelでのentries
変数の初期化(空配列の生成)のSubscribe
を避けるために実施しています
TIPS: .asObservable()
はRxSwift 2.0.0-beta以前では不要でした(ちょっと面倒になった…?)
まとめ
- 値が更新される度に
bind
部分の処理を通るので、プルリフレッシュ、インフィニットロードで再度API通信を行ったときなどもレスポンス後の処理が分散しない - MVVMモデルでViewControllerが肥大化せず可読性も上がるはず
以上です
Swift歴はまだまだ浅いので、さらによい実装方法を勉強させて頂きます!
ありがとうございました