18.
Table & Collection Views
Written by Florent Pillet
The most frequent requirement for iOS applications is to display content in table or collection views. A typical implementation features two or more data source and delegate callbacks, although you often end up with more. RxSwift not only comes with the tools to perfectly integrate observable sequences with tables and collections views, but also reduces the amount of boilerplate code by quite a large amount.
Basic support for UITableView and UICollectionView is present in the RxCocoa framework you were introduced to in previous chapters.
In this chapter, you’ll learn how to quickly wire up tables and collections with just the built-in framework tools. Extended support for things such as sections and animations comes with RxDataSources https://github.com/RxSwiftCommunity/RxDataSources, an advanced framework found under the umbrella of RxSwiftCommunity organization.
The examples below are for UITableView, but the same patterns work for UICollectionView as well.
Basic table view
In a typical scenario, you want to display a list of items of the same type: for example, a list of cities, as you saw in previous chapters. Using standard cells to display them requires nearly zero setup. Consider a single observable list of cities:
@IBOutlet var tableView: UITableView!
func bindTableView() {
let cities = Observable.of(["Lisbon", "Copenhagen", "London", "Madrid", "Vienna"])
cities
.bind(to: tableView.rx.items) {
(tableView: UITableView, index: Int, element: String) in
let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
cell.textLabel?.text = element
return cell
}
.disposed(by: disposeBag)
}
And. That’s. All. You don’t even need to set your UIViewController as a UITableViewDataSource. Wow!
This deserves a quick overview of what’s going on:
-
tableView.rx.itemsis a binder method operating on observable sequences of elements (likeObservable<[String]>). - The binding creates an invisible
ObserverTypeobject which subscribes to your sequence, and sets itself as thedataSourceanddelegateof the table view. - When a new array of elements is delivered on the observable, the binding reloads the table view.
- To obtain the cell for each item, RxCocoa calls your closure with details (and date) for the row being reloaded.
This is straightforward to use. But what if you want to capture the user selection? Again, RxCocoa is here to help:
tableView.rx
.modelSelected(String.self)
.subscribe(onNext: { model in
print("\(model) was selected")
})
.disposed(by: disposeBag)
The modelSelected(_:) method returns an observable which emits the model object (the element represented by the cell) every time the user selects one. An additional variant — itemSelected() — transports the IndexPath of the selected item.
RxCocoa offers a number of observables:
-
modelSelected(_:),modelDeselected(_:),itemSelected,itemDeselectedfire on item selection. -
modelDeleted(_:)fires on item deletion (upontableView:commitEditingStyle:forRowAtIndexPath:). -
itemAccessoryButtonTappedfire on accessory button tap. -
itemInserted,itemDeleted,itemMovedfire on event callbacks in table edit mode. -
willDisplayCell,didEndDisplayingCellfire every time relatedUITableViewDelegatecallbacks fire.
These are all simple wrappers around equivalent UITableViewDelegate methods.
Multiple cell types
It’s nearly as easy to deal with multiple cell types.
From a model standpoint, a good way to handle it is to use an enum with associated data as the element model. Thus, you can handle as many different cell types as you need while binding the table to an observable of arrays of the enum type.
To build a table with cells of just strings, or custom cells with two images, first define a data model with an enum then create an observable of arrays of this model:
enum MyModel {
case text(String)
case pairOfImages(UIImage, UIImage)
}
let observable = Observable<[MyModel]>.just([
.textEntry("Paris"),
.pairOfImages(UIImage(named: "EiffelTower.jpg")!, UIImage(named: "LeLouvre.jpg")!),
.textEntry("London"),
.pairOfImages(UIImage(named: "BigBen.jpg")!, UIImage(named: "BuckinghamPalace.jpg")!)
])
To bind it to the table, use a slightly different closure signature, and load a different cell class depending on the element emitted. The idiomatic code looks like this:
observable.bind(to: tableView.rx.items) {
(tableView: UITableView, index: Int, element: MyModel) in
let indexPath = IndexPath(item: index, section: 0)
switch element {
case .textEntry(let title):
let cell = tableView.dequeueReusableCell(withIdentifier: "titleCell", for: indexPath) as! TextCell
cell.titleLabel.text = title
return cell
case let .pairOfImages(firstImage, secondImage):
let cell = tableView.dequeueReusableCell(withIdentifier: "pairOfImagesCell", for: indexPath) as! ImagesCell
cell.leftImage.image = firstImage
cell.rightImage.image = secondImage
return cell
}
}
.disposed(by: disposeBag)
This is not much more code than before. The only complexity is dealing with multiple data types in the observable of arrays of objects, which you can elegantly solve using an enum. Isn’t Swift great?
Providing additional functionality
Even though RxCocoa-driven table views and collection views don’t require that you set up your view controller as a delegate, you can do so to provide complementary functionality not managed by RxCocoa extensions.
In the case of UICollectionView, you may want to leave your UIViewController as the UICollectionViewDelegate. If you bind this in a nib or storyboard, RxCocoa will do the right thing: it will set itself as the actual delegate, then forward callbacks your view controller implements.
For example when using UICollectionView with manual sizing, you often need to implement collectionView(_:layout:sizeForItemAt:) to compute correct item sizes. If you wired up your collection view with your view controller as its delegate, then later use RxCocoa binding to manage the content, you have nothing special to do. RxCocoa takes care of the details.
If you have already bound your collection view with RxCocoa and want to add your view controller as the collection view delegate, you can simply use this idiom:
tableView.rx
.setDelegate(myDelegateObject)
.disposed(by: disposeBag)
The table’s reactive extension will do the “right thing” and correctly forward your object all delegate methods it implements. Do not directly set your object as the table view or collection view delegate after binding it with RxCocoa. This would prevent some or all of the bindings from working correctly.
RxDataSources
RxCocoa handles the table and collection view needs of many apps. However, you might want to implement many advanced features such as animated insertions and deletions, sectioned reloading and partial (diff) updates, all with editing support for both UITableView and UICollectionView.
Using RxDataSources requires more work to learn its idioms, but offers more powerful, advanced features. Instead of a simple array of data, it requires you to provide contents using objects which conform to the SectionModelType protocol. Each section itself contains the actual objects. For sections with multiple object types, use the enum technique shown above to differentiate the types.
The power of RxDataSources lies in the diff algorithm it uses to determine what’s changed in a model update, and optionally animate the changes. By adopting the AnimatableSectionModelType protocol, your section model can provide details on the sections and models, so RxDataSources can automatically take care of only updating changed, deleted, or newly added cells.
Look up the repository at https://github.com/RxSwiftCommunity/RxDataSources and the included examples to learn more about this advanced framework!