The Developer's Guide to Cell Content Configuration in iOS 14

Have you ever written table view code like this?

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
    let label = // ...
    label.text = "Hello, world"
    return cell
}

Looks simple and innocent enough, but there’s something very wrong with it, philosophically speaking. This is the table view’s data source. What business has the data source knowing that the cell contains a label? The cell is a view. The label is a view. The data source shouldn’t know anything about views. It should supply the cell with data — and that’s all. How the cell reflects that data into the visible world of views should be the cell’s business.

Now, if you’re a mature and professional iOS programmer (and I know that you are, just by looking at you), you’re very well aware of this. Instead of code like what I’ve just shown, what you always do is subclass UITableViewCell and give your subclass a configure method. That way, you can hand the cell the data and tell the cell to configure itself — something like this:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! MyCell
    cell.configure(text: "Hello, world")
    return cell
}

I won’t bother to show how configure(text:) might be implemented; it’s obvious and unimportant. The point is that cells should be self-configuring. You doubtless know this and have known it for a long time.

But Apple hasn’t. Until now.

iOS 14 introduces a whole new architecture for making cells self-configuring. As usual with Apple, this architecture is twice as elaborate and complicated as it needs to be. But you might want to adopt it, so I’m going to tell you about it.

The Idea of a Content Configuration

I’m going to confine myself here to the configuration of the cell’s content. I’m going to ignore the cell’s background, and I’m going to ignore the fact that cells have states (selected or not). In fact, for now, I’m even going to ignore the fact that there are cells! I’m going to start at the very bottom, with Apple’s fundamental idea of what a content configuration is.

I need to establish two definitions:

  • A content configuration (UIContentConfiguration) is an object that produces a content view.

  • A content view (UIContentView) is a view with a settable property that is a content configuration.

In that little circular summary, UIContentConfiguration and UIContentView are protocols. The truth is a little more complicated than what I’ve said, but not much. We can declare a content configuration object (it will usually be a struct) and a content view class, make them adopt those protocols, autogenerate the stubs, fill in the blanks sufficiently to make the code compile, and we get this:

class MyContentView : UIView, UIContentView {
    var configuration: UIContentConfiguration
    init(_ configuration: UIContentConfiguration) {
        self.configuration = configuration
        super.init(frame:.zero)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
struct MyContentConfiguration : UIContentConfiguration {
    func makeContentView() -> UIView & UIContentView {
        return MyContentView(self)
    }
    func updated(for state: UIConfigurationState) -> MyContentConfiguration {
        return self
    }
}

That is a rock-bottom content configuration, along with its content view. The idea here is that a content view contains and configures views, while a content configuration communicates data to the content view — whatever data is needed for the content view to be able to configure its views.

To demonstrate, let’s make our code do something. To keep things simple, my content view is going to contain a single label, centered. So, what data does my configuration need to convey to the content view? Well, a label contains text, so the minimum I’m going to need is that text:

struct MyContentConfiguration : UIContentConfiguration {
    var text = ""
    func makeContentView() -> UIView & UIContentView {
        return MyContentView(self)
    }
    func updated(for state: UIConfigurationState) -> MyContentConfiguration {
        return self
    }
}

All I’ve done so far is to give the configuration object a text property. The workhorse is the content view. Here’s the plan (and this will apply to just about any content configuration’s content view). In the content view, separate the configuration of the view from the application of the data that comes from the configuration object. In the content view’s initializer, configure the content view: give it subviews as needed, for instance. At the end of the content view’s initializer, and in a setter observer for the configuration property, apply the data; that way, the timing doesn’t matter — we can receive the configuration in the initializer, or our configuration can be set later. Like this:

class MyContentView : UIView, UIContentView {
    var configuration: UIContentConfiguration {
        didSet {
            self.configure(configuration: configuration)
        }
    }
    let label = UILabel()
    init(_ configuration: UIContentConfiguration) {
        self.configuration = configuration
        super.init(frame:.zero)
        self.addSubview(self.label)
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            label.topAnchor.constraint(equalTo: self.topAnchor, constant: 10),
            label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -10),
            label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10),
            label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10),
        ])
        self.configure(configuration: configuration)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func configure(configuration: UIContentConfiguration) {
        guard let configuration = configuration as? MyContentConfiguration else { return }
        self.label.text = configuration.text
    }
}

That is pretty much a boilerplate structure for writing a content view. There are some efficiencies we could be adding — for example, if a new configuration is set and it doesn’t differ from the existing configuration, there’s no point setting the label’s text again. But this is a working configuration and content view.

To prove it, let’s display the content view in the interface. It may surprise you that I’m going to do this with no reference to a table view cell! But that is part of the point. This is a general architecture for making a view. So, make an iOS project and add our configuration and content view types to it, and in the view controller’s viewDidLoad, add these lines:

var config = MyContentConfiguration()
config.text = "Hello, world"
let v = MyContentView(config)
v.frame = CGRect(x: 100, y: 100, width: 200, height: 100)
v.backgroundColor = .yellow
self.view.addSubview(v)

Presto, a yellow rectangle with “Hello, world” in the middle has appeared in the interface.

The Idea of a Configurable Cell

So what does all this have to do with table views? Well, you have to know what a table view cell is, in this architecture. It’s a cell with a content configuration property. And here’s the really interesting part: when that property is set, the cell effectively rips out its own contentView and replaces it with the content view that comes from the configuration object.

To see what I mean, let’s migrate our custom configuration object and content view types into another project, one that contains a table view. Here’s the data source method now:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
    var config = MyContentConfiguration()
    config.text = "Hello, world"
    cell.contentConfiguration = config
    return cell
}

Wow, look at that! What we are doing now is exactly what we were saying before that we should do. A cell should be self-configuring; we should just hand it the data. And that is what we are doing. And it works: the words “Hello, world” appear in the middle of the cell.

As you can see, the reason why it works is that a UITableView cell (starting in iOS 14) has a contentConfiguration property, which is (you guessed it) a UIContentConfiguration, which is just what MyContentConfiguration is. Thanks to this property, every table view cell is now self-configuring. You just need a configuration type and a content view type, just as in our example.

To sum up what just happened: we set the cell’s contentConfiguration property to a UIContentConfiguration of some kind. The cell, under the hood, asks the configuration object for its content view (it calls makeContentView). It then rips out its own contentView and replaces it with that content view. And presto! The content view appears in the cell.

Built-in Configurations

So far, we have been building our own UIContentConfiguration type and a content view type to go with it. But it turns out that if what we want is a standard table view cell appearance, we don’t have to do that. Apple already provides a configuration type: it’s called UIListContentConfiguration. And a table view cell will automatically dispense one when we ask for its defaultContentConfiguration.

So now, having gone to all the trouble of writing MyContentConfiguration and MyContentView (to show how this whole thing works under the hood), we can throw them away. Our code now looks like this:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
    var config = cell.defaultContentConfiguration() // *
    config.text = "Hello, world"
    cell.contentConfiguration = config
    return cell
}

The only change here is that we ask the cell for its defaultContentConfiguration. We can still set the configuration’s text property because a UIListContentConfiguration has a text property. (What are the chances of that? It’s almost as if I knew all along where this whole discussion was going, isn’t it?)

The words “Hello, world” now appear at the left end of the cell, not in the middle. That’s the default appearance for a plain basic-type cell. But Apple gives us more UIListContentConfiguration properties that let us tweak that appearance:

var config = cell.defaultContentConfiguration()
config.text = "Hello, world"
config.textProperties.alignment = .center // *
cell.contentConfiguration = config

Now the words “Hello, world” appear in the center of the cell. In fact, this cell looks just like the cell we were creating with MyContentConfiguration a moment ago! I’m not going to tell you about all the other properties of a UIListContentConfiguration; I’ve introduced the architecture and explained how it works, and that was my goal.

WARNING! Apple feels so strongly about the power of UIListContentConfiguration that they are signaling deprecation of the cell properties textLabel, detailTextLabel, and imageView. List content configuration objects have text, secondaryText, and image properties (as well as attributedText and secondaryAttributedText), and you’re supposed to use those instead going forward.

What We Didn’t Cover

There are three big subjects we haven’t covered here.

First: states. Cells have states. The cell might be highlighted/selected. Also, the world around the cell has states. The interface style (light or dark mode) might change, the size class might change, and so on. Content configurations can actually respond to that. Remember this method?

func updated(for state: UIConfigurationState) -> MyContentConfiguration {

We didn’t do anything very interesting with that method; we just returned self. But clearly, there’s more to know here.

Second: backgrounds. I haven’t said anything about the cell background. Apple has also endowed a table view cell with a backgroundConfiguration property, so you can do for the background the sort of thing we did for the content. This is a whole different kettle of fish, however, and needs to be discussed in connection with states.

Third: collection views. A UICollectionView cell also has a contentConfiguration and a backgroundConfiguration. That means almost everything we just said applies a collection view cell. A collection view cell, in general, does not dispense a default configuration, but nothing stops you from making a content configuration object and applying it. You can even use a built-in UIListContentConfiguration object. You can do that in a cell or in a header/footer view (because you can use a UICollectionViewCell where you would use a UICollectionReusableView). (Plus, there is a special kind of collection view cell, UICollectionViewListCell, that does dispense a UIListContentConfiguration as its defaultContentConfiguration.)

I hope to get into all of those topics, and more, in future articles.

Check out more in this series:

The Developer’s Guide to User-Interactive Cell Configurations in iOS 14
The Developer’s Guide to List Content Views in iOS14

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.