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
}}