RxSwiftでUITableView/UICollectionViewのbindを強化する

Friday, February 3, 2017

RxSwiftで良くDataSourceもしくはあるデータの配列をUITableViewやUICollectionViewにbindさせる時に
それをより安全にしたり、bindしつつcellに必要なパラメータを渡せるようにパワーアップさせてみます。

ちなみに例ではRxDataSourceは使わず、データの入った配列をbindする想定でやっていきます。

Cellの型を渡すだけで済むようにする

UITableViewとデータの配列をbindするときに使う関数の中で、次のような関数が用意されています。

func items<S: Sequence, Cell: UITableViewCell, O : ObservableType>
        (cellIdentifier: String, cellType: Cell.Type = Cell.self)
        -> (_ source: O)
        -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
        -> Disposable
        where O.E == S

な、長い…

Cellの再利用時に用いられるidentifierと型を渡すものなのですが、identifierをCellの型名と同じにして管理する場合は、CellReusableのようなprotocolを用意して、
Reactiveに対するextensionの中に関数を生やしてあげると、Cellの型を渡すだけで済むようになります

Cellの再利用時に指定する`identifier`を定義する
protocol CellReusable {
    static var identifier: String { get }
}

extension CellReusable where Self: UITableView {
    static var identifier: String {
        return String(describing: self)
    }
}

//Reactive(BaseがUITableView)に対してextensionを追加する
extension Reactive where Base: UITableView {

   func items<S: Sequence, Cell: UITableViewCell, O: ObservableType>(_ cellType: Cell.Type)
     -> (_ source: O)
     -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
     -> Disposable
     where O.E == S, Cell: CellReusable {
        return items(cellIdentifier: cellType.identifier, cellType: cellType)
    }
}

これによって、itemsのパラメータにCellの型を渡すだけで済むようになります。

class FeedCell: UITableViewCell, CellReusable {
}

let list: Observable<[...]> = ...


list.bindTo(tableView.rx.items(FeedCell.self)) { _, item, cell in
    // ...
}
.addDisposableTo(disposeBag)

少し便利になりましたね!

bindしつつ、Cellにパラメータを渡してしまう

Cellのクラスにconfigure(with:) みたいな、パラメータを渡してCellのセットアップをする関数を用意して、
データの配列をbindしつつ、データを渡してみます。

struct FeedItem {
    // ...
}
class FeedCell: UITableViewCell, CellReusable {
    func configure(with feedItem: FeedItem) {
        // ...
    }
}

let list: Observable<[FeedItem]> = ...


list.bindTo(tableView.rx.items(FeedCell.self)) { _, item, cell in
    cell.configure(with: item)
}
.addDisposableTo(disposeBag)

ごくごく普通なのですが、これをCellが 何かしらのprotocolに適合していたら 自動的にcellにパラメータを渡すようにしてみたいと思います。

まずは、 CellConfigurableなるprotocolを定義します

protocol CellConfigurable {
    associatedtype Parameter
    func configure(with parameter: Parameter)
}

このCellConfigurableに適合させる場合には、configure(with:)で渡す時のParameterの型を指定することが必須になります。

次に、 Cellの型を渡すだけで済むようにする で紹介したものと組み合わせて、以下のようにReactiveに対するextensionに関数を追加します。

extension Reactive where Base: UITableView {

   func items<S: Sequence, Cell: UITableViewCell, O: ObservableType>(cellType: Cell.Type)
        -> (_ source: O)
        -> Disposable
        where O.E == S, Cell: CellReusable & CellConfigurable, Cell.Parameter == S.Iterator.Element {
        return { source in
            let configureCell: (Int, S.Iterator.Element, Cell) -> Void = { index, parameter, cell in
                cell.configure(with: parameter)
            }
            return self.items(cellIdentifier: cellType.identifier, cellType: cellType)(source)(configureCell)
        }
    }
}

CellCellReusable と CellConfigurable に適合している」 且つ、
「データの配列の要素の型 S.Iterator.Elementと、Cell.Parameterが一致する」
という条件を与えてあげます。 こうすることで、

struct FeedItem {
    // ...
}
class FeedCell: UITableViewCell, CellReusable, CellConfigurable {
    typealias Parameter = FeedItem
    func configure(with parameter: Parameter) {
        // ...
    }
}

let feedList: Observable<[FeedItem]> = ...


feedList.bindTo(tableView.rx.items(FeedCell.self))
    .addDisposableTo(disposeBag)

と、スッキリさせることができます。

上記だと、 (Int, S.Iterator.Element, Cell) -> Voidのclosureを受け取れないので、受け取りたい時は、

   func items<S: Sequence, Cell: UITableViewCell, O: ObservableType>(cellType: Cell.Type)
        -> (_ source: O)
        -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
        -> Disposable
        where O.E == S, Cell: CellReusable & CellConfigurable, Cell.Parameter == S.Iterator.Element {
        return { source in
            return { configureCell in
                let _configureCell: (Int, S.Iterator.Element, Cell) -> Void = { index, parameter, cell in
                    cell.configure(with: parameter)
                    configureCell(index, parameter, cell)
                }
                return self.items(cellIdentifier: cellType.identifier, cellType: cellType)(source)(_configureCell)
            }
        }
    }

も別途宣言してあげると

let feedList: Observable<[FeedItem]> = ...


feedList.bindTo(tableView.rx.items(FeedCell.self)) { _, item, cell in
        // 既にcellに配列の要素が渡された状態で返却される
        print(item, cell)
    }
    .addDisposableTo(disposeBag)

のように使うことができます。

UICollectionViewの場合は

若干書き方が異なるかもしれないですが、ほぼ同じようにできると思います。 ここでは省略します。

tarunon/Instantiateを使ってみる

最後になりますが、
tarunonさんのtarunon/Instantiateを使って書き換えてみた場合を紹介します。
Instantiateを使うと、次のようになります。

import RxSwift
import RxCocoa
import Instantiate

extension Reactive where Base: UITableView {
   func items<S: Sequence, Cell: UITableViewCell, O: ObservableType>(cellType: Cell.Type)
        -> (_ source: O)
        -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
        -> Disposable
        where O.E == S, Cell: Reusable {
        return items(cellIdentifier: cellType.reusableIdentifier, cellType: cellType)
    }

   func items<S: Sequence, Cell: UITableViewCell, O: ObservableType>(cellType: Cell.Type)
        -> (_ source: O)
        -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
        -> Disposable
        where O.E == S, Cell: Reusable & Bindable, Cell.Parameter == S.Iterator.Element {
        return { source in
            return { configureCell in
                let _configureCell: (Int, S.Iterator.Element, Cell) -> Void = { index, parameter, cell in
                    cell.bind(to: parameter)
                    configureCell(index, parameter, cell)
                }
                return self.items(cellIdentifier: cellType.reusableIdentifier, cellType: cellType)(source)(_configureCell)
            }
        }
    }

   func items<S: Sequence, Cell: UITableViewCell, O: ObservableType>(cellType: Cell.Type)
        -> (_ source: O)
        -> Disposable
        where O.E == S, Cell: Reusable & Bindable, Cell.Parameter == S.Iterator.Element {
        return { source in
            let configureCell: (Int, S.Iterator.Element, Cell) -> Void = { index, parameter, cell in
                cell.bind(to: parameter)
            }
            return self.items(cellIdentifier: cellType.reusableIdentifier, cellType: cellType)(source)(configureCell)
        }
    }

}

実はこのInstantiateが最近パワーアップしたのと、ご本人のツイートを見て、ちょっとやってみようということでやってみました。

こちらのライブラリはStoryBoardやXibからView(Controller)を生成する部分や、CellのReuseに関するものなど、UIKitを素のままで使うとイマイチな部分をprotocolで安全に素敵な実装ができるようになります。

protocolの分離の仕方等がとても参考になります。ふつくしい。

techSwiftRxSwiftprotocol

`git push -u` で消耗しないために

Gitでリモートブランチを追跡しつつチェックアウトするのに消耗しないために