RxDataSourceを使ってUITableViewのデータバインディングを実装するテンプレートを作ってみた

概要

今回はRxSwiftの機能をまとめたUITableView用のデータバインディングライブラリRxDataSourceを使ってUITableViewを実装していく方法を解説します。

github.com

RxDataSourceの概要を説明すると

Writing table and collection view data sources is tedious. There is a large number of delegate methods that need to be implemented for the simplest case possible.

TableviewおよびCollectionViewのdataSourceを作成するのは面倒です。可能な限り最も単純な場合に実装する必要がある多数のデリゲートメソッドがあります。

RxSwift helps alleviate some of the burden with a simple data binding mechanism:

RxSwiftは、単純なdata binding mechanismによって、負担を軽減するのに役立ちます。

  1. Turn your data into an Observable sequence
  2. Bind the data to the tableView/collectionView using one of:

1). データをObservableなシーケンスに変換する
2). 次のいずれかを使用して、データをtableView / collectionViewにバインドします。

  • rx.items(dataSource:protocol<RxTableViewDataSourceType, UITableViewDataSource>)
  • rx.items(cellIdentifier:String)
  • rx.items(cellIdentifier:String:Cell.Type:_:)
  • rx.items(::)

例えば、下記のような構文があります。

let data = Observable<[String]>.just(["first element", "second element", "third element"])

data.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { index, model, cell in
  cell.textLabel?.text = model
}
.disposed(by: disposeBag)

This works well with simple data sets but does not handle cases where you need to bind complex data sets with multiples sections, or when you need to perform animations when adding/modifying/deleting items.

これは単純なデータの設定ではうまく機能しますが、複雑なデータの設定となると複数のセクションでバインドする必要がある場合や、アイテムを追加/変更/削除するときにアニメーションを実行する必要がある場合は扱えません。

These are precisely the use cases that RxDataSources helps solve.

これらはまさにRxDataSourcesが解決に役立つユースケースです。

With RxDataSources, it is super easy to just write

RxDataSourcesを使えばこのように書くだけでとても簡単です。

let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Int>>(configureCell: configureCell)
Observable.just([SectionModel(model: "title", items: [1, 2, 3])])
    .bind(to: tableView.rx.items(dataSource: dataSource))
    .disposed(by: disposeBag)

とこんな風に書くことが可能になります。
(ただ、私としてはタイプ量が変わらないのでどっちもどっち感は少なからずあったりします。)

実務ではこのRxDataSourceを使ったアプリもあると思います。 そこで今回はこのRxDataSourceを使って簡単なテンプレートを作成してみました。

storyboardについて

今回は一度ソースコードをリセットしてVCを一つだけにしています。

Main.storyboard

f:id:qed805:20190224145732p:plain
rxdatasource_3

このようにTableViewとUITableViewCell(identifierにはCellをセットしておく)を乗せています。

podfileについて

今回は新しくしたのでこのように編集しています。

Podfile

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'RxTableView' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for RxTableView
    pod 'RxSwift', '~> 4.0'  # 追加する
    pod 'RxCocoa', '~> 4.0'  # 追加する
    pod 'RxDataSources', '~> 3.0'

  target 'RxTableViewTests' do
    inherit! :search_paths
    # Pods for testing
  end

  target 'RxTableViewUITests' do
    inherit! :search_paths
    # Pods for testing
  end

end

ソースコードについて

では実際にRxDataSourceを使った場合のソースコードをまとめて書いてみます。

ViewController.swift

import UIKit
import RxSwift
import RxCocoa
import RxDataSources

/// TableViewCellに紐付けるDataModel
struct CustomCellModel {
    var name: String
    var email: String
}

/// セクションヘッダーの名前とセクション内のitem
struct SectionOfCustomData {
    /// セクションヘッダーの名前
    var header: String
    /// indexPath.row のcellのデータ
    var items: [Item]
}

/// RxDataSourceを使ってDataModelとdataSourceを紐づけるため
extension SectionOfCustomData: SectionModelType {
    // Item に CellのDataModelを紐づける
    typealias Item = CustomCellModel
    
    // ほぼテンプレで可能
    init(original: SectionOfCustomData, items: [Item]) {
        self = original
        self.items = items
    }
}

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!

    let disposeBag = DisposeBag()
    
    let dataSource = RxTableViewSectionedReloadDataSource<SectionOfCustomData>(
        configureCell: { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = item.name
            return cell
    })
    
    // 複数のsectionを表示させたい場合はSectionOfCustomDataを生成していきます。
    // SectionOfCustomData の
    let sections = [
        SectionOfCustomData(header: "1st section",
                            items: [CustomCellModel(name: "山田花子", email: "hanako@gmail.com"),
                                    CustomCellModel(name: "田中太郎", email: "taro@gmail.com"),
                                    CustomCellModel(name: "石田真一", email: "shinichi@gmail.com")]),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }
}

以上となります。
ソースコードの部分にコメントを書いています。
このコメントの意味を理解すればRxDataSourceの基本的な使い方が分かるように説明しています。

このコードをビルドするとアプリは次のように表示されます。

f:id:qed805:20190224143944p:plain
rxdatasource_2

実際にアプリでデータが表示されたことが確認できました。
RxDataSourceに慣れるとよりtableViewやcollectionViewでより複雑なレイアウトを実装するのが楽になりますので挑戦してみてもいいかもしれませんね。 おそらくCustomHeaderViewやCustomFooterViewが必要なアプリだとRxDataSourceに頼らざるを得ない印象はあります。