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