iOS
The Developer's Guide to List Content Views in iOS 14
Matt Neuburg
Written on December 17, 2020

In an earlier article, I introduced iOS 14’s new content configuration architecture. You have a UIContentConfiguration object and a UIContentView, and they go together. Configuring the content configuration object expresses the data you want represented; the configuration object then generates the content view, which constructs the interface that displays that data.
As I showed in that article, you can make your own UIContentConfiguration and UIContentView if you like, and you can use the resulting content view anywhere in your interface. One area of the interface that is already set up for this is a cell, meaning a UITableViewCell or a UICollectionViewCell:
-
A cell has a
contentConfiguration
property; whatever content view the cell’s content configuration creates, the cell makes that its owncontentView
, automatically. -
To help you even more, UITableViewCell vends a
defaultContentConfiguration
which is a built-in type: UIListContentConfiguration. You can use this to describe a simple cell that can displaytext
,secondaryText
, and animage
.
What I didn’t tell you at the time is what sort of content view a UIListContentConfiguration creates. It’s another built-in type, a UIListContentView. In this article, I’m going to talk more about this view.
I’ve Got a Little List
A UIListContentView is, not surprisingly, a UIView. That means you can put it anywhere in your interface. And that means that your interface can include something that looks like a table view cell without actually being one.
That’s very cool because there are often times when you want something like a small list without going to all the trouble of making a full-fledged table view. Apple has already made some concessions in this direction; there is such a thing as a static table view. But a static table view is still a table view, and it has to be configured in the storyboard — and what if you’re not using a storyboard? With a list content view, you don’t even have to use a table view in the first place.
To illustrate, I’ll display the names and images of the three Pep Boys in an interface that looks rather like a table, but is not in fact a UITableView. Instead, it will be a simple vertical UIStackView of three list content views. I have the images in my asset catalog already, and the stack view is already in my interface (self.stack
):
for pep in ["Manny", "Moe", "Jack"] {
var config = UIListContentConfiguration.cell()
config.text = pep
config.image = UIImage(named: pep)
let v = UIListContentView(configuration: config)
self.stack.addArrangedSubview(v)
}
To understand that code, start by reading it backward. The goal is to add to three list content views as arranged subviews to our stack view. To make a list content view, you need a list content configuration. To make a list content configuration, you call a class method that effectively describes the style of the configuration; I’ve chosen the cell()
style here. And that’s all there is to it: as far as the interface is concerned, we’ve got a little list!
The Joy of Lightweight Lists
To continue the demonstration, let’s do something downright useful with this list: let’s make it possible for the user to tap a Pep Boy as a way of picking a favorite. This really is the sort of thing that I’ve used table views for in the past. For example, in one of my game apps, there’s a preferences interface where the user gets to decide on one of two styles and one of three sizes. On a Mac desktop, I suppose I’d be using radio buttons at this point. But iOS doesn’t have radio buttons. So I use a table view.
That works fine, but it’s rather heavyweight considering the simplicity of the job to be done. Not only do I have to populate the table view dynamically using a data source, but I also need to place the checkmark in the correct cell, as well as responding when the user actually taps a cell. The code isn’t complicated, but it’s very definitely table view code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellid, for:indexPath)
let section = indexPath.section
let row = indexPath.row
switch section {
case 0:
cell.textLabel!.text = Styles.styles()[row]
case 1:
cell.textLabel!.text = Sizes.sizes()[row]
default:
break
}
cell.textLabel?.font = UIFont.systemFont(ofSize: 17, weight: .regular)
cell.accessoryType = .none
let currentDefaults = [ud.string(forKey: Default.style), ud.string(forKey: Default.size)]
if currentDefaults.contains(where: {$0 == cell.textLabel!.text}) {
cell.accessoryType = .checkmark
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let setting = tableView.cellForRow(at: indexPath)?.textLabel?.text {
ud.set(setting, forKey: indexPath.section == 0 ? Default.style : Default.size)
tableView.reloadData()
}
}
How would we do the same sort of thing using our lightweight list consisting of just list content views? Well, the nice thing is that these are just views. There is no data source that keeps getting called to produce the cells; the views are static, and we can manipulate them directly.
To detect the user’s tap gesture, I’ve attached a tap gesture recognizer to the stack view. When the user taps, I work out which list content view contains the tap, and I read the text value right out of its configuration. (I don’t actually approve of misusing a view as data, but it’s only an example, so just bear with me.)
@objc func didTap(_ gr: UIGestureRecognizer) {
guard let v = gr.view, gr.state == .ended else { return }
if let pep = v.hitTest(gr.location(in: v), with: nil) as? UIListContentView {
if let config = pep.configuration as? UIListContentConfiguration {
if let which = config.text {
self.currentFavorite = which
self.checkFavorite() // *
}
}
}
}
We have now recorded who the favorite is, and our goal is to put a checkmark into the corresponding view. This raises the question of how we’re going to add our own custom views to a content view that doesn’t belong to us. Let’s say we want to put the checkmark right after the label that displays the configuration’s text
. This could be an image view displaying a checkmark symbol image:
func checkmarkView() -> UIImageView {
let iv = UIImageView(image: UIImage(systemName: "checkmark")!)
iv.translatesAutoresizingMaskIntoConstraints = false
iv.tintColor = .label
iv.tag = 100
return iv
}
So far, so good. But we have no access to the label, so how can we position the checkmark view relative to it?
It turns out that Apple has thought of this. They want you to be able to mix your own interface into a list content view. So a UIListContentView has three UILayoutGuide properties marking the location of the labels and the image view:
textLayoutGuide
secondaryTextLayoutGuide
imageLayoutGuide
Using autolayout, we can pin our checkmark view directly to the trailing edge of the text label. In this implementation, I just rip out all the checkmark views and insert a new one into the correct list content view:
func checkFavorite() {
for pep in self.stack.arrangedSubviews as! [UIListContentView] {
if let check = pep.viewWithTag(100) { check.removeFromSuperview() }
if let config = pep.configuration as? UIListContentConfiguration {
if let which = config.text, which == self.currentFavorite {
let iv = self.checkmarkView()
pep.addSubview(iv)
if let text = pep.textLayoutGuide {
iv.leadingAnchor.constraint(equalTo: text.trailingAnchor, constant: 20).isActive = true
iv.centerYAnchor.constraint(equalTo: text.centerYAnchor).isActive = true
}
}
}
}
}
Lists in Your Life
UIListContentConfiguration and UIListContentView are pervasive features of the iOS 14 API. They make it easy to configure a simple table view cell, a simple collection view cell, or a collection view that looks like a table view.
What I’ve suggested in this article is that you should also be thinking about places to use a UIListContentView outside of any cell. It’s an ordinary view and can appear anywhere in your interface. If you think over your apps and their interfaces, there are probably lots of places where you have short, simple lists that you implemented as a table view because that’s the tool that was ready to hand. A content configuration and its content view might be a better way. And in many cases, Apple’s own built-in list content view might be all you need.
And everything about list content views outside of a cell remains true when the content view is inside a cell. You can add your own subviews there too, and position them relative to the existing views, just as I did with my checkmark view in the example.
There is also much more to know about list content configurations themselves. Using them, you get quite a lot of power to customize the views that the list content view will display. You have no direct access to the labels and the image view, but you have indirect access through the configuration’s textProperties
, secondaryTextProperties
, and imageProperties
. Moreover, you get some say in the positioning of these views: you can set the padding between them, as well as the overall margins.
Check out the rest of this series:
The Developer’s Guide to Cell Content Configuration in iOS14
The Developer’s Guide to User-Interactive Cell Configurations in iOS 14