RxSwiftを使ってiOSのUIとデータをバインドするときに、ちょっと躓きやすいのがUITableViewとのバインディングです。少なくとも自分は躓きました。
ということで、調べたことをまとめておきます。
例として Item
というクラスの配列が流れてくる items
というObservableを、UITableView(tableView
)とバインドすることを考えます。
パターンとしては、次の4つがあります。
パターン 1
let items: Observable<[Item]> = ...
items
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { row, element, cell in
// row: Int …… アイテムのインデックス
// element: Item …… アイテムのインスタンス
// cell: UITableViewCell …… セルのインスタンス
// ここでセルの中身を設定する
cell.textLabel?.text = element.name
}
.disposed(by: disposeBag)
最もシンプルなのがこのパターンです。
-
rx.items
の第1引数にidentifierを渡す。 - クロージャーの中で、セルに値をセットする。
パターン 2
let items: Observable<[Item]> = ...
items
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: MyTableViewCell.self)) { row, element, cell in
// row: Int …… アイテムのインデックス
// element: Item …… アイテムのインスタンス
// cell: MyTableViewCell …… セルのインスタンス
// ここでセルの中身を設定する
cell.nameLabel.text = element.name
cell.ageLabel.text = "\(element.age)"
}
.disposed(by: disposeBag)
パターン 1ではクロージャーに渡ってくる cell
の型が UITableViewCell
でしたが、カスタムセルを定義しているような場合は、その型を指定することができます。
もちろん、あらかじめCell Identifierとカスタムセルのクラスの結びつけをStoryboardで設定しておくか、 register(_:forCellReuseIdentifier:)
で登録しておく必要があります。
パターン 3
let items: Observable<[Item]> = ...
items
.bind(to: tableView.rx.items) { tableView, row, element in
// tableView: UITableView …… テーブルビュー
// row: Int …… アイテムのインデックス
// element: Item …… アイテムのインスタンス
// ここでセルを作って、中身を設定する
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = element.name
return cell
}
.disposed(by: disposeBag)
パターン 1、2ではidentifierを引数に指定していましたが、パターン 3ではセルを作るところがクロージャーの中に入ります。
データによって、複数のカスタムセルをそれぞれのidentifierで使い分けているような場合はこちらを使うことになります。
このクロージャーの中でやることは、 UITableViewDataSource
の tableView(_:cellForRowAt:)
を実装するときとほぼ同じと考えていいと思います。
パターン 4
let items: Observable<[Item]> = ...
let dataSource = ...
items
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
パターン 1〜3 と違って、データソースを自分で指定する方法です。
これが一番なんでもできるパターンです。
例えば、 UITableViewDataSource
の tableView(_:canEditRowAt:)
を実装しなきゃいけないとか、セクションのヘッダーやフッターが必要といった、個別のデータソースの実装が必要な場合はこちらを使います。
ただし、渡す dataSource
の型はRxTableViewDataSourceType & UITableViewDataSource
なので、 単純に UITableViewDataSource
に準拠するだけでなく、 RxTableViewDataSourceType
にも準拠する必要があります。
データソースの例
ひとまず、次に挙げるだけの実装を行えば、上記のようにバインドして使えるものになります。
class MyDataSource: NSObject, UITableViewDataSource, RxTableViewDataSourceType {
typealias Element = [Item]
var _itemModels: [Item] = []
// MARK: UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return _itemModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let element = _itemModels[indexPath.row]
cell.textLabel?.text = element.name
return cell
}
// MARK: RxTableViewDataSourceType
func tableView(_ tableView: UITableView, observedEvent: Event<Element>) {
Binder(self) { dataSource, element in
dataSource._itemModels = element
tableView.reloadData()
}
.on(observedEvent)
}
}
RxTableViewDataSourceType
が必要とするメソッド tableView(_:observedEvent:)
では、Observerに次のデータがやってきたときの処理を書くことになります。
この例では、ここで _itemModels
を更新して reloadData
を呼び出すことでUITableViewを更新しています。
ここを工夫して beginUpdates
、 endUpdates
、 insertRows(at:with:)
、 deleteRows(at:with:)
……などを使うようにすれば、アニメーションを伴う更新も可能です。
セル選択時の modelSelected
に対応させる
rx.modelSelected
を使いたければ、データソースは SectionedViewDataSourceType
にも準拠させる必要があります。
次のように実装しておけばいいでしょう。
class MyDataSource: NSObject, UITableViewDataSource, RxTableViewDataSourceType, SectionedViewDataSourceType {
... // 上記の実装
// MARK: SectionedViewDataSourceType
func model(at indexPath: IndexPath) throws -> Any {
return _itemModels[indexPath.row]
}
}
おまけ
下記のRxSwiftCommunity/RxDataSourcesで、パターン 4のデータソースとして使える便利なものが公開されています。そのまま使うこともできるし、自分でデータソースを作るときの参考にもなると思います。