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?
Configuration cells are the concept used to manage parallel simulation in Simics. Each cell contains a set of objects that can be run in parallel to objects in other cells, but that cannot run in parallel to objects in the same cell.
The visual representation of a single row in a table view. iOS 2.0+ iPadOS 2.0+ Mac Catalyst 13.1+ tvOS 9.0+
An object that manages an ordered collection of data items and presents them using customizable layouts.
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
}}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With