Creating tables with UICollectionView in iOS14

Table views have been a central part of the iOS user interface since the first version of the platform, while collection views were a much later addition. Over the last couple of iOS releases, collection views have become steadily more functional - to the point where with iOS14, UITableView is (almost) a thing of the past. They’re not officially deprecated - yet - but the writing is on the wall…

This is a walkthough of a minimum viable table view implementation using UICollectionView, as a worked example of how the more powerful user interface control can now replicate its older sibling.

The full code is available on Github.

The basic project

You’ll need to create a new iOS app, using Storyboard interfaces and the UIKit App Delegate lifecycle. The minimum requirements are Swift 5, Xcode 12 and iOS14.

Modelling the items and sections

The items that will be displayed in the table are modelled as simple structs with three properties:

 struct Item: Hashable {
    var title: String
    var subtitle: String
    var image: UIImage

The collection view’s sections are modelled using an enum - we’ll use two sections:

enum Section {
    case main
    case second

View Controller properties

In the view controller, create two properties for the collection view and its data source:

 var collectionView: UICollectionView!
 var datasource: UICollectionViewDiffableDataSource<Section, Item>!

The collection view’s data

We’ll create two arrays of Items to model the data for each section. Add two computed properties to the controller:

lazy var mainSectionItems = (1...10).map { index -> Item in
    return Item(title: "Item \(index)", 
                subtitle: "First section", 
                image: UIImage(named: "unicorn")!)

lazy var secondSectionItems = (1...10).map { index -> Item in
    return Item(title: "Element \(index)",
                subtitle: "Second section", 
                image: UIImage(named: "panda")!)

Configuring the collection view layout

We’ll use the UICollectionLayoutListConfiguration layout with an insetGrouped appearance. The layout needs to be available when we configure the collection view, so we’ll add a function that returns a UICollectionViewLayout object, and call this as we set up the collection view:

private func configureLayout() -> UICollectionViewLayout {
    let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
    return UICollectionViewCompositionalLayout.list(using: config)

Configuring the collection view

We’ll configure the collection view in a function that’s called from viewDidLoad. This will set constraints to fill the full screen, set the view controller class as the collection view’s delegate, and add it into the view hierarchy:

private func configureCollectionView() {
    collectionView = UICollectionView(frame: view.bounds, 
                                      collectionViewLayout: self.configureLayout())
    collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    collectionView.delegate = self

The collection view is instantiated with the view’s bound, and a layout generated by the configureLayout function.

Don’t forget to call configureCollectionView from viewDidLoad:

override func viewDidLoad() {

Configuring the data source

Add another function to the view controller called configureDataSource, and call this from viewDidLoad.

Configuring the data source takes place in two stages. First, we create a cell registration closure that uses a UICollectionViewListCell, and updates the cell’s content with values from the relevant Item:

let cellRegistration = 
  UICollectionView.CellRegistration<UICollectionViewListCell, Item> { 
      (cell, indexPath, item) in
        var content = cell.defaultContentConfiguration()
        content.text = item.title
        content.textProperties.color =
        content.secondaryText = item.subtitle
        content.image = item.image
        cell.contentConfiguration = content

Next, we instantiate the datasource and give it a cellProvider closure. This dequeues a cell from the collection view using the cell registation closure for configuration:

datasource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, 
   cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
      return collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
                                                            for: indexPath, item: item)

Setting up the data

With the datasource configured, the only step left is to apply the initial data set to it. Add a applyInitialData function to the class - this creates an instance of NSDiffableDataSourceSnapshot and appends the two sections.

Then the items are appended to the relevant section, and the snapshot is applied to the datasource.

private func applyInitialData() {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
    snapshot.appendSections([.main, .second])
    snapshot.appendItems(mainSectionItems, toSection: .main)
    snapshot.appendItems(secondSectionItems, toSection: .second)
    datasource.apply(snapshot, animatingDifferences: false)

Call this new function from viewDidLoad:

override func viewDidLoad() {

The end results

Build and run the project, and you’ll see a collection view that looks and behaves exactly like a table view:


Further tweaks

The Github project also implements a stub delegate method to illustrate handling selection in the collection view.

UICollectionView can mimic different table view styles by changing the list configuration appearance. This example uses the insetGrouped appearance, but you’ve also got the option of plain and sidebar styles. If those standard styles aren’t suitable, the cellRegistration closure gives you the opportunity to completely customise the appearance of the cell.

Using these techniques, it’s possible to implement a collection view which looks and feels exactly like a table view. Given the additional features that a collection view offers, it seems reasonably likely that UITableView will be deprecated in the not-too distant future.