29

I'm watching and rewatching WWDC 2020 "Modern cell configuration" but enlightening does not strike.

I understand that the built-in ListContentConfiguration has properties similar to the built-in cell style components like the text label and the image view. But I never use the built-in cell styles; I always create a cell subclass and design the cell subviews from scratch in a xib or storyboard prototype cell, or even structure them in code.

So how do I use Apple's configurations to populate my cell's subviews?

The video says:

In addition to the list content configuration we're also giving you access to the associated list content view which implements all of the rendering. You just create or update this view using the configuration, and then you can add it as a subview right alongside your own custom views. This lets you take advantage of all the content configuration features and combine the ListContentView with your own additional custom views next to it, such as an extra image view or a label.

OK, no, that's not what I want to do. I don't want any of the built-in cell style subviews.

So then the video says:

Even when you're building a completely custom view hierarchy inside your cells, you can still use the system configurations to help. Because configurations are so lightweight, you can use them as a source of default values for things like fonts, colors, and margins that you copy over to your custom views, even if you never apply the configuration directly itself. And for more advanced use cases you can create a completely custom content configuration type with a paired content view class that renders it, and then use your custom configuration with any cell the same way that you would use a list content configuration.

My italics, and the italics are part I'm asking about. I'm asking: how?

  • I understand that there is a UIContentConfiguration protocol.

  • I understand that the conforming class generates through its makeContentView method a "content view", a UIView with a configuration property (because it conforms to UIContentConfiguration).

So how do I use that in conjunction with my custom cell subclass, to communicate information from the data source to the cell and populate the cell's subviews?

As usual, it feels like Apple shows us the toy examples and completely omits details about how this can work in the real world. Has anyone figured this out?

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • The latest version of the [Implementing Modern Collection Views sample code](https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views) includes an example of how to get started with a custom content configuration. – smileyborg Aug 04 '20 at 05:56
  • 2
    @smileyborg Indeed, and so does my answer. What I’m saying is that that should have been in the video. Just like in the UISplitViewController video, they walk right up to the interesting, important part and then suddenly walk away from it again with no explanation. – matt Aug 04 '20 at 11:21

2 Answers2

27

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • why do you need MyCell as custom cell? Is it possible to have one cell for multiple items that are not connected? As you pointed, ContentConfiguration is a data source for content view, so, we need only a different content views for different items. Do we need different cells for different items? – gaussblurinc Sep 13 '20 at 19:24
  • 1
    @gaussblurinc I do not understand the words "one cell for multiple items that are not connected". But of course you are right, I do not actually need MyCell as a custom cell; I just did that so as to namespace the configuration and content view classes (note `MyCell.Configuration`). I was assuming one configuration per cell type, but that's just an assumption. In fact, I don't need a cell at all; part of Apple's point about this architecture is that a configuration-plus-content-view just gives you a self-configuring view that you can put _anywhere_. – matt Sep 13 '20 at 19:44
  • I am playing with Apple example. BackgroundConfiguration is a struct ( unlike ContentConfiguration which is protocol ). So, if I want to add custom background configuration, I should subclass a cell... – gaussblurinc Sep 13 '20 at 23:13
  • @gaussblurinc I too find it annoying that UIBackgroundConfiguration is a struct, not a class or a protocol. Especially because I think Apple did a really bad job on the UIBackgroundConfiguration API; it has no `selectedBackgroundView`, for example, so you really are forced into a cell subclass solution. – matt Sep 27 '20 at 02:47
  • 1
    I want to thank you for that article, I think the new APIs are awesome, despite some drawbacks (like, where is the `contentConfiguration` property for UICollectionView supplementary views?) I got rid of cell subclasses in my project completely in favour of `UIContentView`'s – Adam Nov 21 '21 at 11:35
1

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
}}
Neph Muw
  • 780
  • 7
  • 10