how to use iOS 14 cell content configurations in general?

Edit I have now published a series of articles on this topic, starting with https://www.biteinteractive.com/cell-content-configuration-in-ios-14/.


The key here — and I don't think that Apple has made this clear at all in the videos — is that the way these cell configurations work is by literally ripping out the cell's contentView and replacing it with the view supplied by the configuration as the output of its makeContentView.

So all you have to do is build the entire content view by hand, and the runtime will put it in the cell for you.

Here's an example. We need to supply our own configuration type that adopts UIContentConfiguration, so that we can define our own properties; it must also implement makeContentView() and updated(for:). So pretend we have four texts to display in the cell:

struct Configuration : UIContentConfiguration {
    let text1: String
    let text2: String
    let text3: String
    let text4: String
    func makeContentView() -> UIView & UIContentView {
        let c = MyContentView(configuration: self)
        return c
    }
    func updated(for state: UIConfigurationState) -> MyCell.Configuration {
        return self
    }
}

In real life, we might respond to a change in state by changing returning a version of this configuration with some property changed, but in this case there is nothing to do, so we just return self.

We have posited the existence of MyContentView, a UIView subclass that adopts UIContentView, meaning that it has a configuration property. This is where we configure the view's subviews and apply the configuration. In this case, applying the configuration means simply setting the text of four labels. I'll separate those two tasks:

class MyContentView: UIView, UIContentView {
    var configuration: UIContentConfiguration {
        didSet {
            self.configure()
        }
    }
    private let lab1 = UILabel()
    private let lab2 = UILabel()
    private let lab3 = UILabel()
    private let lab4 = UILabel()
    init(configuration: UIContentConfiguration) {
        self.configuration = configuration
        super.init(frame: .zero)
        // ... configure the subviews ...
        // ... and add them as subviews to self ...
        self.configure()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    private func configure() {
        guard let config = self.configuration as? Configuration else { return }
        self.lab1.text = config.text1
        self.lab2.text = config.text2
        self.lab3.text = config.text3
        self.lab4.text = config.text4
    }
}

You can see the point of that architecture. If at some point in the future we are assigned a new configuration, we simply call configure to set the texts of the labels again, with no need to reconstruct the subviews themselves. In real life, we can gain some further efficiency by examining the incoming configuration; if it is identical to the current configuration, there's no need to call self.configure() again.

The upshot is that we can now talk like this in our tableView(_:cellForRowAt:) implementation:

override func tableView(_ tableView: UITableView, 
    cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(
            withIdentifier: self.cellID, for: indexPath) as! MyCell
        let config = MyCell.Configuration(
            text1: "Harpo",
            text2: "Groucho",
            text3: "Chico",
            text4: "Zeppo"
        )
        cell.contentConfiguration = config
        return cell
}

All of that is very clever, but unfortunately it seems that the content view interface must be created in code — we can't load the cell ready-made from a nib, because the content view loaded from the nib, along with all its subviews, will be replaced by the content view returned from our makeContentView implementation. So Apple's configuration architecture can't be used with a cell that you've designed in the storyboard or a .xib file. That's a pity but I don't see any way around it.


Project on GitHub

From Xcode 12, iOS 14 Table View Cell Configuration:

struct CityCellConfiguration: UIContentConfiguration, Hashable {
var image: UIImage? = nil
var cityName: String? = nil
var fafourited: Bool? = false

func makeContentView() -> UIView & UIContentView {
    return CustomContentView(configuration: self)
}

func updated(for state: UIConfigurationState) -> Self {
    guard let state = state as? UICellConfigurationState else { return self }
    let updatedConfig = self

    return updatedConfig
}}

Apply configuration:

private func apply(configuration: CityCellConfiguration) {
    guard appliedConfiguration != configuration else { return }
    appliedConfiguration = configuration
    
    imageView.isHidden = configuration.image == nil
    imageView.image = configuration.image
    textLabel.isHidden = configuration.cityName == nil
    textLabel.text = configuration.cityName
    favouriteButton.isFavourited = configuration.fafourited ?? false
}

Update configuration inside cell:

override func updateConfiguration(using state: UICellConfigurationState) {
    var content = CityCellConfiguration().updated(for: state)
    content.image = "🏢".image()
    if let item = state.item {
        content.cityName = item.name
        if let data = item.imageData {
            content.image = UIImage(data: data)
        }
    }
    contentConfiguration = content
}

Implement Table View Data Source:

extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return cities.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = (tableView.dequeueReusableCell(withIdentifier: Configuration.cellReuseIdentifier) ?? CityTableViewCell(style: .value1, reuseIdentifier: Configuration.cellReuseIdentifier)) as? CityTableViewCell else {
        return UITableViewCell(style: .value1, reuseIdentifier: Configuration.cellReuseIdentifier)
    }
    
    let city = cities[indexPath.row]
    cell.updateWithItem(city)

    return cell
}}