Collection View Lists in iOS 14

One of the most significant innovations in iOS 14 is that a collection view can be configured to look and behave like a table view. A collection view of that kind is called a collection view list.

A collection view list isn’t a matter of mere layout. Laying out a collection view as a single vertically scrolling column of cells, which is how a table view is laid out, has always been possible. A collection view list also acts like a table view. A collection view list cell can have accessory views, such as the disclosure indicator that tells the user to tap to navigate. It can have swipe actions, such as the user’s ability to swipe left to see a Delete button. It can have an edit mode. And so on.

He’s Making a List

To illustrate, let’s make a list. Lists are a kind of compositional layout. (I assume you know what those are; compositional collection view layouts were introduced in iOS 13, so you’ve had a year to get used to them by now.) How you specify that you want a list depends on whether this list constitutes the whole collection view or a section of the compositional layout:

  • Entire collection view: To ask for a list layout for the entire collection view, create a UICollectionLayoutListConfiguration and pass it into the UICollectionViewCompositionalLayout list(using:) static method. Now use the result as your collection view’s layout.

  • Compositional layout section: To ask for a list layout for a section of a compositional list, create the layout with a section provider function, and call the NSCollectionLayoutSection static method list(using:layoutEnvironment:) to create the section.

Either way, we need a UICollectionLayoutListConfiguration object in order to get started. The UICollectionLayoutListConfiguration initializer takes an appearance: value that corresponds to the style of a table view: .plain, .grouped, and so on. You can also use the configuration to determine the overall background color and the visibility of separators.

I’m going to reuse an example that I’ve been using for years as my table view and collection view testbed. The idea is to display the names of the U.S. states, arranged alphabetically and divided into sections by the first letter of the state name. I’m going to use a diffable data source; my data consists of a string section title and a string state name, so I’ll declare an instance property self.datasource, like this:

var datasource : UICollectionViewDiffableDataSource<String,String>!

Everything else happens in my view controller’s viewDidLoad. Let’s specify that we want a single list that constitutes the whole collection view and that looks like a plain table view:

let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)

We now set that layout as our collection view’s collectionViewLayout, and we’re ready to configure the list.

I’ll use a cell registration object, and I’ll take advantage of the UIListContentConfiguration architecture to get the state name into the cell interface (and see my earlier article for more about those new iOS 14 collection view features):

let cellreg = UICollectionView.CellRegistration<UICollectionViewCell, String> { cell, ip, s in
    var config = UIListContentConfiguration.cell()
    config.text = s
    cell.contentConfiguration = config
}
self.datasource = UICollectionViewDiffableDataSource<String,String>(collectionView:cv) { cv, ip, s in
    cv.dequeueConfiguredReusableCell(using: cellreg, for: ip, item: s)
}

Now I’ll populate the diffable data source. I’ve got a text file consisting of the names of the states in alphabetical order. I’ll read the text file, clump it by the first letters of the states, construct a snapshot, and apply it:

let s = try! String(contentsOfFile: Bundle.main.path(forResource: "states", ofType: "txt")!)
let states = s.components(separatedBy:"\n").filter {!$0.isEmpty}
let d = Dictionary(grouping: states) {String($0.prefix(1))}
let sections = Array(d).sorted {$0.key < $1.key}
var snap = NSDiffableDataSourceSnapshot<String,String>()
for section in sections {
    snap.appendSections([section.0])
    snap.appendItems(section.1)
}
self.datasource.apply(snap, animatingDifferences: false)

Already we see something in the interface!

simple list

However, there are no section headers appearing, even though our data source is clumped into sections. Let’s fix that now.

Seeing Sections

If we want headers or footers, we have to say so back when we create the UICollectionLayoutListConfiguration. There’s a headerMode and a footerMode, and the default value is .none, which is why we don’t see anything. If we want to see a header, we need to set the headerMode to .supplementary:

var config = UICollectionLayoutListConfiguration(appearance: .plain)
config.headerMode = .supplementary

(Alternatively, we could treat an ordinary cell as a header by setting the headerMode to firstItemInSection. This is very nice because it allows us to use a flat list as our data model, and we are saved the trouble of dealing explicitly with a separate supplementary view. However, supplementary views are more interesting, so that’s what I’ll demonstrate.)

We are now going to have a demand for a supplementary view coming at us. And we must supply one, or we will crash. I’ll assign the diffable data source a supplementaryViewProvider function, and again I’ll use a registration object. When we make a registration object for supplementary views, we must specify the kind string that signifies the type of supplementary view. We also need to specify a view type, which must be a UICollectionReusableView. I’ll use UICollectionViewCell, which is eligible because it’s a UICollectionReusableView subclass — because that way, I can use the contentConfiguration again. While I’m up, I’ll use the backgroundConfiguration as well:

let kind = UICollectionView.elementKindSectionHeader
let headreg = UICollectionView.SupplementaryRegistration<UICollectionViewCell>(elementKind: kind) { cell, kind, ip in
    let section = ip.section
    var config = UIListContentConfiguration.cell()
    config.text = self.datasource.snapshot().sectionIdentifiers[section]
    config.textProperties.font = UIFont(name:"Georgia-Bold", size:22)!
    config.textProperties.color = .green
    cell.contentConfiguration = config
    var back = UIBackgroundConfiguration.listPlainHeaderFooter()
    back.backgroundColor = .black
    cell.backgroundConfiguration = back
}
self.datasource.supplementaryViewProvider = { cv, kind, ip in
    cv.dequeueConfiguredReusableSupplementary(using: headreg, for: ip)
}

Presto, we’ve got sections!

list with section headers

The Height Report

You may be wondering what governs the height of our cells and our section header views. This is not a table view, so there is no table view rowHeight property, and there are no height delegate methods. And although this is a compositional layout, we are not constructing it from scratch, so we have no opportunity to configure an NSCollectionLayoutGroup with an NSCollectionLayoutSize. Instead, autolayout is being used internally, and if we don’t like that, we have to use a UICollectionReusableView subclass where we override preferredLayoutAttributesFitting(_:). However, we may be able to compensate by governing the size of the internal subviews instead.

To illustrate, I’ll add an image to our section headers:

config.image = UIImage(named:"us_flag_small.gif")

The result is that the section headers are rather tall because the image is a little too large:

big flags

I can shrink the image, though, using an imageProperties setting:

config.image = UIImage(named:"us_flag_small.gif")
config.imageProperties.maximumSize = CGSize(width:0, height:20)

The result is that the flags are a little smaller and the section headers are a little shorter:

list flags

Making a Selection

Our collection view is looking and behaving a lot like a table view. At the moment, however, when the user taps on a cell, nothing happens. If we want some code to run when the user taps a cell, that’s a previously solved problem; this is still a UICollectionView, so there’s a delegate method for that:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    // whatever
}

Nevertheless, nothing is happening visibly in the cell; we are not getting any sort of indication of selection. The reason for that is simple: I’m using the wrong cell type! Remember back at the start, when I declared that my list cells should be a plain UICollectionViewCell?

let cellreg = UICollectionView.CellRegistration<UICollectionViewCell, String> { cell, ip, s in

That’s wrong. There’s a cell subclass intended especially for use in collection view lists, called (wait for it) UICollectionViewListCell. Let’s use that instead:

let cellreg = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, ip, s in

Presto, tapping a cell now causes the default gray to appear, indicating that the cell is selected.

What’s more, a collection view list cell has a defaultContentConfiguration, just like a table view cell. Let’s change our code to take advantage of that. We currently have this:

let cellreg = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, ip, s in
    var config = UIListContentConfiguration.cell()

We should change it to this:

let cellreg = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, ip, s in
    var config = cell.defaultContentConfiguration()

Using a list cell brings with it a number of other benefits; I’ll talk about them in a separate article.

If It Quacks Like a Duck

Our collection view list now looks and behaves a lot like a table view. The user will be unaware that anything new is happening here. The interface will have a familiar look and feel; whether this is a table view or a collection view list under the hood makes no significant difference to the user’s experience. If it looks like a duck, and walks like a duck, and quacks like a duck, it’s a list.

I have not yet explained how to give a collection view list additional features familiar from table views, such as accessory views and swipe actions. I’ll postpone that to another time.

There is some current buzz to the effect that the arrival on the scene of collection view lists somehow signals the impending death of UITableViews as an independent entity. The reasoning, I suppose, is that a collection view can imitate a table view, so table views are otiose. I regard this as a canard. There’s no evidence that table views are going away; collection view lists can’t do everything that table views can do, nor do they need to.

Nevertheless, Apple would like you to use collection view lists. And for simple lists, at least, collection view lists are easy to implement. I’ve shown here that getting started with collection view lists is straightforward.

You Might Also Like…

Collection View Outlines in iOS 14

In an earlier article, I introduced collection view lists, new in iOS 14, which allow a collection view, or a section of a collection view, to imitate a table view. Collection view lists are the basis for yet another new iOS 14 feature: outlines. As Lovely as a Tree An outline is a way of …

Collection View Outlines in iOS 14 Read More »

    Sign Up

    Get more articles and exclusive content that takes your iOS skills to the next level.