Binding a list with multiple sections and different cells
Today’s post showcases another real-life use case of using CombineDataSources, namely binding a list to a table view with multiple sections using different cell types.
In my last post Binding a simple list to a UITableView we had a look at binding a list of data elements to a table view. It’s pretty simple to bind a plain table view when you have a coherent list of elements.
In this post I’m gonna show some code on how to bind a list to a table with multiple sections that also needs to use different cell types for the different sections.
In the previous post I used the rowsSubscriber(...)
extension on UITableView
like so:
bind(subscriber:
tableView.rowsSubscriber(
cellIdentifier:
cellType:
cellConfig:
)
)
tableView.rowsSubscriber(...)
returns a table specific subscriber which manages the cells in the table view. The subscriber does that by instantiating a TableViewItemsController
configured with the given cell type & identifier.
Given you set a single cell identifier, this approach obviously suits only plain lists with a single type of table rows.
In this post we are going to go a level deeper into the API and create manually a TableViewItemsController
which will allow us to create different types of cells and feed them to the table view.
Setting up a sectioned table project
To get the CombineDataSources binding goign first of all I need a simple project with a sectioned table view to play with.
For this post I created a simple Mail app featuring a table view list with two cells - one for inbox messages and one for outbox messages:
Each cell is a different type (InboxCell
and OutboxCell
) and features its own custom UI in the Storyboard.
I also need a data model for the inbox and outbox messages:
struct Message: Hashable {
let title: String
let text: String
}
let messages: [[Message]] = [
[
Message(title: "Re: Hello again!", text: "...")
],
[
Message(title: "Hello again!", text: "..."),
Message(title: "Please confirm your account", text: "..."),
Message(title: "See you at: Swift Peer Lab", text: "...")
]
]
Message
is a simple struct that includes a title and the message text. messages
is a Message
collection that I’ll bind to my table view: the first array in messages
is my outbox message list, the second contains the messages in the inbox.
Binding multiple cell types
I’d like to bind my messages
list to the two sections of my table - outbox and inbox.
I’m not going to use the same API like in my previous post mentioned above but create a TableViewItemsController
manually instead.
TableViewItemsController
has two initializers (modelled after the RxDataSources API):
init(cellIdentifier:cellType:cellConfig:)
- this is the initializer thatUITableView.rowsSubscriber(...)
uses.init(cellFactory:)
- this is the initializer that we’re going to use below.
The cellFactory
closure is given the table view, the controller, and the data and is expected to dequeue a cell and return it ready for displaying.
I’m gonna do that my view controller’s viewDidLoad()
method:
let listController = TableViewItemsController<[[Message]]>
{ (controller, table, indexPath, message) -> UITableViewCell in
}
TableViewItemsController
is generic over the list type it binds, which in my case is [[Message]]
- one [Message]
list for each of the table’s sections.
The closure parameter receives four parameters as follows:
- the controller instance itself
- the table view
- the index path of the current row
- the data element to bind for this row
Since I have a different cell type per section I will switch over the current row’s index path and decide what cell to return. The closure body becomes:
switch indexPath.section {
case 0: // Outbox section
let cell = table.dequeueReusableCell(
withIdentifier: OutboxCell.identifier) as! OutboxCell
cell.title.text = message.title
return cell
case 1: // Inbox section
let cell = table.dequeueReusableCell(
withIdentifier: InboxCell.identifier) as! InboxCell
cell.title.text = message.title
return cell
default: fatalError()
}
In each of my cases I’m free to configure the cells as I please - above I just set the row’s title.
With an initialized controller in hand, I simply need to bind the message list to a table subscriber. Right after the code from above, I’ll add:
Just(messages)
.bind(subscriber: tableView.sectionsSubscriber(listController))
.store(in: &subscriptions)
tableView.sectionsSubscriber(listController)
creates a sections subscriber by using the given listController
controller.
I bind my message list to that subscriber and that’s all I need to wrap up this example. (Storing the subscription in a [AnyCancellable]
as well.)
Running the project I get a populated table with the expected rows types in the two sections:
That’s all for today, I hope that was useful and I’ll see you next time!
Where to go from here?
In case you missed the previous post on CombineDataSources you can find it here: Binding a simple list to a UITableView.
To learn more about working with UIKit and Combine check out Combine: Asynchronous programming with Swift - this is where you can see all updates, discuss in the website forums, and more.