Driver
RxSwift/Documentation/Units.md #Driverにも記載がある通り、
UIレイヤーのリアクティブプログラミングのために提供されている部品 らしいと最近認識しました
以前記事にしたRxSwiftを使ったRSSFeed取得のサンプルでは
Viewの更新にも以下のようにVariable
変数を使っていたのですが、
// entriesはVariable変数
viewModel.entries.asObservable()
.subscribe(onNext: { [unowned self] x in
// 更新
self.refresh.endRefreshing()
self.tableView.reloadData()
}, onError: { error in
// エラー
// FIXME: いつかやるかも
}, onCompleted: { () in
// 完了
}) { () in
}.addDisposableTo(disposeBag)
これってDriverを使うべきなのでは・・・?
ということで、Swiftビギナーな自分が四苦八苦しながらRxSwiftでUIの更新にはDriverを使ってみたまとめです
どんなアプリ?
: 以前作ったRSSFeedアプリのサンプルコードをリファクタリングしてみました
環境
- Xcode 7.3
- Swift 2.2
- RxSwift 2.3.1
- iOS8以上
リポジトリ
##概要
過去の記事にあります
Driverを使う
イメージです
Driverの使い所
1. Model : (準備) リクエスト結果をVariable
に反映してViewModelに通知
ここでは、リクエストを実行し結果をSubscribe
して各Variable
値を更新します
これでViewModel側に各値の変更が通知されます
この時API通信はバックグランドで行うように指定しておきます
/// Load
final let isLoading = Variable(false)
/// Entries
final let entries = Variable([Entry]())
/// Error
final let error: Variable<ErrorType?> = Variable(nil)
/// Rx Dispose
final let disposeBag = DisposeBag()
final func request() {
if isLoading.value {
return
}
isLoading.value = true
error.value = nil
let request = FeedRequest()
request.connect()
// API接続はBackgroundを指定する
.subscribeOn(SerialDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.subscribe(
onNext: { [weak self] in self?.entries.value = $0.feed.entries },
onError: { [weak self] in
self?.error.value = $0
self?.isLoading.value = false },
onCompleted: { [weak self] in self?.isLoading.value = false }
)
.addDisposableTo(disposeBag)
}
それぞれView側のUIと紐付きます
- isLoading:UIRefreshControlの表示・非表示
- entries:UITableView上に表示する一覧データ
- error:エラー時用のUIViewの表示・非表示
2. ViewModel : モデルから受け取った値をDriver
に変換する
リクエストで受け取った結果をDriver
に変換します
TIPS: Driver.never()
しておくと値を持たずCompletedすら発生しません
TIPS: Driver
を使うにはimport RxCocoa
が必要でした
/// Request Model class
final private var model = ListModel()
/// Entries
final var entries = [Entry]()
// Rx
/// Data Updated
final private(set) var dataUpdated: Driver<[Entry]> = Driver.never()
/// Loading flg
final private(set) var isLoading: Driver<Bool> = Driver.never()
/// Error flg
final private(set) var isError: Driver<Bool> = Driver.never()
override init() {
super.init()
// ViewController側でTableViewの更新と紐付けます
dataUpdated = Driver
// エラーのとき、一覧に表示するエントリーを0件にするように変換しています
.combineLatest(model.entries.asDriver(),
model.error.asDriver().map { $0 != nil },
resultSelector: { ($1) ? [] : $0 })
// ViewController側でRefreshControlのステータスと紐付けます
isLoading = model.isLoading.asDriver()
// ViewController側でエラー画面の表示と紐付けます
isError = model.error.asDriver().map { $0 != nil }
}
1.のリクエストでVariable
が更新されるたびに、Driver
からはobserverに対して通知が行われます
ViewControllerではこの通知をトリガーにして、バインドしている各UIの更新を行います
3. ViewController : ViewModelのDriver
をUIにバインドする
ViewDidLoad()
内でDriver
と各UIを紐付けます
// TableView Reload
viewModel.dataUpdated
.driveNext { [unowned self] in
self.viewModel.entries = $0
self.tableView.reloadData()
}
.addDisposableTo(disposeBag)
// Loading Status
viewModel.isLoading
.drive(refresh.rx_refreshing)
.addDisposableTo(disposeBag)
// Error View
viewModel.isError
.map { !$0 }
.drive(errorView.rx_hidden)
.addDisposableTo(disposeBag)
// Pull Refresh
refresh.rx_controlEvent(.ValueChanged)
.subscribeNext { [unowned self] _ in self.viewModel.reloadData()
}.addDisposableTo(disposeBag)
これで、リクエストをして結果が更新されるたびにDriver
からはobserverに対して通知が行われ、バインドしている各UIの更新が働きました
できたもの
まとめ
-
Driver
はメインスレッド処理に適している - エラー発生しない
以上です
苦しみながら書きました!RxSwiftは難しいです・・(勉強会参加したかった)
Modelクラスなど、もっとかっこいい設計がありそうです。
今後もさらによい実装方法を勉強させて頂きます!
読んでいただき、ありがとうございました