The Developer's Guide to Cell Background Configuration in iOS 14

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 own contentView, 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 display text, secondaryText, and an image.

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 the selectedBackgroundView when multiple selection is enabled. If not defined separately, the selectedBackgroundView 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 and isHighlighted.

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

You Might Also Like…

Taking Control of Rotation Animations in iOS

We all love to have animation in our app’s interface. But sometimes writing even the simplest animation can be rather elusive. Rotation is a case in point. As you probably know, the way to rotate a view (or a layer) is to change its transform, applying a rotation transform to it. But certain rotations are …

Taking Control of Rotation Animations in iOS Read More »

    Sign Up

    Get more articles and exclusive content that takes your iOS skills to the next level.