iOS
The Developer's Guide to Cell Background Configuration in iOS 14
Matt Neuburg
Written on December 29, 2020

In an earlier article, I introduced iOS 14’s new content configuration architecture. As I showed in that article, this architecture is useful particularly in a cell, meaning a UITableViewCell or a UICollectionViewCell:
-
A cell has a
contentConfiguration
property; whatever content view the cell’s content configuration creates, the cell makes that its owncontentView
, automatically. -
To help you even more, UITableViewCell vends a
defaultContentConfiguration
which is a built-in type: UIListContentConfiguration. You can use this to describe a simple cell that can displaytext
,secondaryText
, and animage
.
In that article, I talked about the cell’s content; I didn’t say anything about the background of the cell. Now I’m going to discuss that.
On Background
As you are doubtless aware, a UITableViewCell has background-related properties that you can use to configure its background:
backgroundColor
: A cell is a view, so it has a background color like any other view.backgroundView
: A view that is layered in front of the background color (but behind the content).selectedBackgroundView
: A view that is layered in front of the background view (but behind the content) when the cell is highlighted or selected. If you don’t supply a background view, a default gray selected background view is used.multipleSelectionBackgroundView
: Used instead of theselectedBackgroundView
when multiple selection is enabled. If not defined separately, theselectedBackgroundView
is used.
There is also a selectionStyle
property. If you set this to .none
, then neither the default gray selected background view nor your custom selectedBackgroundView
(or multipleSelectionBackgroundView
) will appear.
If you like, you can go right on using that way of configuring a table cell’s background, even while adopting the iOS 14 contentConfiguration
architecture to set the cell’s content. However, a cell also has a backgroundConfiguration
property, whose value must be a UIBackgroundConfiguration instance (a struct).
A UIBackgroundConfiguration has properties for describing the cell’s layer, such as its cornerRadius
, strokeColor
, and strokeWidth
. In addition, it has backgroundColor
and backgroundView
properties; these are the properties I’m particularly interested in here. For example:
var b = UIBackgroundConfiguration.listPlainCell()
b.backgroundColor = .blue
b.customView = UIImageView(image: UIImage(named: "linen"))
cell.backgroundConfiguration = b
In the configured cell, both of the UIBackgroundConfiguration background-related properties are actually represented by views; the backgroundView
is drawn in front of a view that portrays the backgroundColor
.
As soon you set the cell’s backgroundConfiguration
to a UIBackgroundConfiguration, you effectively cancel out the four background-related cell properties. For example, you cannot have both a backgroundConfiguration
and a selectedBackgroundView
. Once you have a background configuration, you are going to be using the configuration architecture exclusively to describe the cell’s background.
WARNING: If you explicitly set the background configuration’s backgroundColor
to nil
, the inherited tintColor
is used to derive the background color.
What About Selection?
So far, background configuration is sounding pretty straightforward, yes? It will not have escaped your attention, however, that in my description, I have said nothing about cell selection. There is no selectedCustomView
. What are we supposed to do about customizing the cell’s appearance in response to the cell being selected?
This is a serious puzzle. A UIBackgroundConfiguration has an updated(for:)
method whose parameter is a UIConfigurationState (just like a UIContentConfiguration). Presumably, this method is called when the cell’s state changes. But you have no access to this method! UIBackgroundConfiguration isn’t a class, so you can’t subclass it. And it isn’t a protocol, so you can’t adopt it. In that sense, it’s difficult to see what this method is even for; in a very real sense, it might as well not exist.
A cell, on the other hand, has an updateConfiguration(using:)
method. A cell is a class, so we can subclass it and override this method:
class MyCell: UITableViewCell {
override func updateConfiguration(using state: UICellConfigurationState) {
// what?
super.updateConfiguration(using:state)
}
}
The incoming parameter is a UICellConfigurationState object. This is a value struct (just a bunch of properties) consisting of two sets of data:
-
A
traitCollection
property. This property is acquired through adoption of the UIConfigurationState protocol. -
Properties related to the state of the cell. For our current purposes, the most important are
isSelected
andisHighlighted
.
So this method is called whenever the environment’s trait collection changes and whenever the cell’s selection state changes. We’re interested in the cell being selected or highlighted vs. neither:
class MyCell: UITableViewCell {
override func updateConfiguration(using state: UICellConfigurationState) {
if state.isSelected || state.isHighlighted {
// what?
} else {
// what?
}
super.updateConfiguration(using:state)
}
}
Clearly, what we want to do is alter the cell’s background configuration. The cell’s background configuration does not arrive as a parameter into this method; on the other hand, we are the cell, so we can just fetch the background configuration and change it.
Let’s say that our goal is to overlay a translucent gray color on top of the cell’s existing custom background view as a signal to the user that the cell is selected. That sounds like a view — a subview of the existing image view that is functioning as the configuration’s customView
. We can simply add and remove that subview as circumstances warrant; moreover, we have reference-type access to the custom view, so we don’t even need to replace the background configuration:
class MyCell: UITableViewCell {
override func updateConfiguration(using state: UICellConfigurationState) {
guard let cv = self.backgroundConfiguration?.customView else { return }
if state.isSelected || state.isHighlighted {
if cv.subviews.count == 0 {
let sel = UIView()
sel.backgroundColor = UIColor.gray.withAlphaComponent(0.3)
sel.frame = cv.bounds
sel.autoresizingMask = [.flexibleWidth, .flexibleHeight]
cv.addSubview(sel)
}
} else {
cv.subviews.first?.removeFromSuperview()
}
super.updateConfiguration(using:state)
}
}
Color Transformers
The architecture I’ve just described is surprising. We are unable to configure the cell’s selection-related behavior in the data source; we are compelled to subclass the cell and respond to selection ourselves. I guess I don’t mind having to write the code that responds to selection, but I really don’t like having to subclass the cell merely to say what code it is. I think I would have expected a UIBackgroundConfiguration to have something like an onSelectionChange
property whose value is a function to be called by the runtime.
That, in fact, is what we get if all we want to do is change the background color dynamically (without regard to the background custom view). A UIBackgroundConfiguration, it turns out, has a backgroundColorTransformer
property. It is a function, and it is called when there’s a state change:
var b = UIBackgroundConfiguration.listPlainCell()
b.backgroundColorTransformer = // ...
cell.backgroundConfiguration = b
What should the transformer function be? It needs to receive a UIColor and return a UIColor. It has no access to the cell state; on the other hand, if we define the function in our cellForRowAt
implementation, we can pass a reference to the cell directly into the function (because a Swift function is a closure). Here, we can examine the configuration state and act accordingly. In this example, my background color is blue, except when the cell is selected, in which case it is gray:
var b = UIBackgroundConfiguration.listPlainCell()
b.backgroundColorTransformer = UIConfigurationColorTransformer { [weak cell] c in
if let state = cell?.configurationState {
if state.isSelected || state.isHighlighted {
return .gray
}
}
return .blue
}
cell.backgroundConfiguration = b
You’ll notice that I did nothing with the incoming color parameter (c
). The identity of the incoming color seems rather capricious and I don’t want to go into detail about it. Observe also that I did not set the background configuration’s backgroundColor
in that example. My experimentation shows that if you do that, the color returned by the backgroundColorTransformer
is not used. You must use one or the other, it seems. Logging shows that even when I am returning gray, the background color displayed is not turning gray. The whole mechanism feels rather buggy to me.
Conclusions
I like content configurations, and I can see why Apple feels sufficiently confident about them to deprecate table view cell subviews such as textLabel
and imageView
. Background configuration, on the other hand, seems somewhat misconceived. Their behavior feels buggy, and the requirement that we subclass the cell just to specify a different background view when the cell is selected is onerous and unnecessarily heavyweight. Going forward, I’m happy to use cell content configurations, but I have a feeling I’m going to be sticking with the cell’s backgroundView
and selectedBackgroundView
for a while yet.
Let me remind you once more, also, that everything I’ve said about table view cell configurations applies equally to collection view cells. A UICollectionViewCell has a contentConfiguration
and a backgroundConfiguration
. A collection view cell doesn’t have a textLabel
or an imageView
, but if you apply a UIListContentConfiguration to the cell, you effectively get them for free. You can use the backgroundConfiguration
or you can stick with the cell’s backgroundView
and selectedBackgroundView
.
Check out the rest of the series:
The Developer’s Guide to User-Interactive Cell Configurations in iOS 14
The Developer’s Guide to List Content Views in iOS 14
The Developer’s Guide to Cell Content Configuration in iOS 14