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:

Mail app storyboard

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 that UITableView.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:

Mail app running

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.