February 3, 2017

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

Categories: 技術 | Tags: #Swift #RxSwift #protocol


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の分離の仕方等がとても参考になります。ふつくしい。


written by sgr-ksmt