190
173

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RxSwiftのUITableViewとのバインディングまとめ

Last updated at Posted at 2016-12-31

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で使い分けているような場合はこちらを使うことになります。
このクロージャーの中でやることは、 UITableViewDataSourcetableView(_:cellForRowAt:) を実装するときとほぼ同じと考えていいと思います。

パターン 4

let items: Observable<[Item]> = ...

let dataSource = ...
items
    .bind(to: tableView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

パターン 1〜3 と違って、データソースを自分で指定する方法です。
これが一番なんでもできるパターンです。
例えば、 UITableViewDataSourcetableView(_: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を更新しています。
ここを工夫して beginUpdatesendUpdatesinsertRows(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のデータソースとして使える便利なものが公開されています。そのまま使うこともできるし、自分でデータソースを作るときの参考にもなると思います。

190
173
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
190
173

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?