Binding to a table view with multiple cells and sections
25/Dec 2018
One of the questions that keeps popping up in the RxSwift Slack channel for years now is how to display multiple cell types when binding data to a table view.
This is actually very easy to do and in this post I’ll show you two distinct ways to display multiple cells in a table view (and it works identically for collection views if that’s what you’re looking for).
We’ll look at the following two use cases:
- binding items via using RxCocoa’s
bind(to:)
operator, - using
RxDataSources
to bind cells in different table sections.
Let’s get started!
Easy binding via RxCocoa and bind(to:)
If you do not need multiple table sections or you do not want to add RxDataSources as an extra dependency in your project you can bind directly via bind(to:)
.
I’ve got a simple table view controller with two separate cells - one is displaying “standard” items in my list and the other “important” ones (the latter is just styled a bit more fancy):
The two cells have identifiers as follows: “Cell” and “ImportantCell”.
In my view controller’s viewDidLoad(_:)
method I’m making an observable of string values to display in the table view:
let items = Observable.just([
"First",
"(New) Second",
"(New) Third",
"Fourth"
])
The goal here will be to bind this observable to the table and display any items starting with “(New)” via the pre-designed important cell.
Next I’m adding two helper factory methods that will produce standard and important cells when needed:
private func makeCell(with element: String, from table: UITableView) -> UITableViewCell {
let cell = table.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel!.text = element
return cell
}
private func makeImportantCell(with element: String, from table: UITableView) -> UITableViewCell {
let cell = table.dequeueReusableCell(withIdentifier: "ImportantCell")!
cell.textLabel!.text = element
cell.textLabel!.textColor = .red
return cell
}
makeCell
instantiates a cell with “Cell” identifier and sets its label to the given string. makeImportantCell
similarly makes an instance of “ImportantCell” and also applies some styling to the label.
Now I need to call the correct factory method when binding data. Back in viewDidLoad(_:)
I’m adding the binding code:
items.bind(to: tableView.rx.items) { table, index, element in
if element.hasPrefix("(New)") {
return self.makeImportantCell(with: element, from: table)
} else {
return self.makeCell(with: element, from: table)
}
}
.disposed(by: bag)
And there you have it - this code binds the items
observable sequence to the table view items and provides a factory closure that instantiates each cell that the table will display.
In this example it’s enough to use hasPrefix(_:)
to diversify between the cell types to return, but of couse in your own code you can have as complex rules as you need.
Running the code displays my list on screen with the correct cells:
Using multiple data models
More often that not you will have a list of data models (usually some custom structs) instead of a list of strings that you’d like to bind to a table view.
That’s easy enough to as well, define a custom enum
with the different data models you will be binding and do a switch in the cell factory closure. For example:
enum MyCellModels {
case former(FirstModel)
case latter(SecondModel)
}
Now your observable should emit a sequence of MyCellModels
instances and you’re good to go. The next section actually features an example of this approach so we won’t go into more details right here and now.
Binding multiple table sections via RxDataSources
Another use case for binding multiple cell types is when your table features multiple sections - sometimes different sections display different kinds of data and naturally you’ll use different cells to do that.
The default binding via RxCocoa does not support sections but the well known RxDataSources library does that so we’ll have a look at how to use it with multiple cell types.
The concept is similar to what we looked at in the first section of this post but here we’ll take the code a bit further.
For this example I’m going to use exactly the same view controller in my storyboard:
This time around though I’m going to add RxDataSources to a new view controller class and do things slightly differently.
First I’m going to model my data. In this example the standard items in my table are going to be driven by a list of strings, but the important items are going to have a dedicated data model struct called Important
. (This is done mostly so that I can show you how to model more compelx data bindings.)
Let’s start with Important
:
struct Important {
let text: String
let imporance: Int
}
The data model for each important cell in the table includes a text to display on screen and an importance level (an arbitrary number for demo purposes).
Next I’m going to build the type for my observable sequence, which should accomodate for either String
items or Important
items:
enum CellModel {
case standard(String)
case important(Important)
}
Now I can create a sequence of type CellModel
and bind it to the table view.
I’m going to copy my cell factory methods from the previous section, but adjust the latter one to accomodate for displaying the custom Important
model like so:
private func makeCell(with element: String, from table: UITableView) -> UITableViewCell {
let cell = table.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel!.text = element
return cell
}
private func makeImportantCell(with element: Important, from table: UITableView) -> UITableViewCell {
let cell = table.dequeueReusableCell(withIdentifier: "ImportantCell")!
cell.textLabel!.text = element.text
cell.textLabel!.textColor = .red
return cell
}
With all that prep finished let’s move to the view controller’s viewDidLoad(_:)
where I’m going to create the observable and bind it to the table view.
First I’m going to create the observable:
let sections = Observable.just([
SectionModel(model: "Standard Items", items: [
CellModel.standard("First item"),
CellModel.standard("Second item")
]),
SectionModel(model: "Important Items", items: [
CellModel.important(.init(text: "Third item", imporance: 1)),
CellModel.important(.init(text: "Fourth item", imporance: 100))
])
])
When using RxDataSources to bind sectioned data you need to emit section models. You can create your own section model types or (as above) you can use the example, pre-defined SectionModel
.
Each SectionModel
in the list contains a section name and a list of CellModel
items.
The first section contains CellModel.standard
items and the second section contains CellModel.important
items. In case your observable updates the table content dynamically you need to re-emit exactly the same structure each time.
Next I’m going to create my custom data source for the binding:
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, CellModel>>(configureCell: { dataSource, table, indexPath, item in
switch item {
case .standard(let text):
return self.makeCell(with: text, from: table)
case .important(let important):
return self.makeImportantCell(with: important, from: table)
}
})
I’m creating a RxTableViewSectionedReloadDataSource
which is set to emit SectionModel<String, CellModel>
items. In the cell factory closure I just use a switch
over each item and return the cell appropriate for displaying each item.
Additionally to make the output a bit easier to understand let’s configure the headers for each section as well:
dataSource.titleForHeaderInSection = { dataSource, index in
return dataSource.sectionModels[index].model
}
With the data source ready, the last bit is to actually bind the observable to the table view:
sections
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
And here’s the result in the iPhone Simulator:
As you saw, when you ignore all the setup and data code the actual binding is just few lines - RxDataSources really shines when it comes to handling more complex data bindings.
Where to go from here?
No matter if you are looking for an easy and quick binding via RxCocoa’s built-in bind(to:)
or binding a more complex data model via RxDataSources, it’s always just few lines of code! I hope you enjoyed this post!
In case you are interested in binding data from the Realm database specificaly, I got some posts in here that cover this as well.
To learn more about RxSwift and testing check out the RxBook! The book is available at http://raywenderlich.com/store - this is where you can see all updates, discuss in the website forums, etc.
Hope that post was helpful, and if you want to get in touch you can find me here Follow @icanzilb
Share this post: Tweet