Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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のデータソースとして使える便利なものが公開されています。そのまま使うこともできるし、自分でデータソースを作るときの参考にもなると思います。

https://github.com/RxSwiftCommunity/RxDataSources

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away