Setting the `backgroundColor` property of UIBackgroundConfiguration breaks the default UIConfigurationColorTransformer

The following is a UIKit app that displays a collection view with list layout and diffable data source (one section, one row).

class ViewController: UIViewController {
    var collectionView: UICollectionView!
    
    var dataSource: UICollectionViewDiffableDataSource<String, String>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureHierarchy()
        configureDataSource()
    }
    
    func configureHierarchy() {
        collectionView = .init(frame: .zero, collectionViewLayout: createLayout())
        view.addSubview(collectionView)
        collectionView.frame = view.bounds
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }
    
    func createLayout() -> UICollectionViewLayout {
        UICollectionViewCompositionalLayout { section, layoutEnvironment in
            let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
            return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
        }
    }
    
    func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
            var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
            backgroundConfiguration.backgroundColor = .systemBlue
            cell.backgroundConfiguration = backgroundConfiguration
        }
        
        dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        }
        
        var snapshot = NSDiffableDataSourceSnapshot<String, String>()
        snapshot.appendSections(["main"])
        snapshot.appendItems(["demo"])
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

If you tap on the row, it seems like selection doesn't happen: giving the cell a blue background broke its default background color transformer.

Here's what I've tried and didn't work:

  1. Setting the collection view's delegate and specifying that you can select any row
  2. Setting the color transformer to .grayscale
  3. Setting the backgroundConfiguration to UIBackgroundConfiguration.listGroupedCell().updated(for: cell.configurationState)
  4. Combinations of the approaches above
  5. Setting the color transformer to UIBackgroundConfiguration.listGroupedCell().backgroundColorTransformer and cell.backgroundConfiguration?.backgroundColorTransformer (they're both nil)
  6. Setting the cell's backgroundColor directly

I also considered using a custom color transformer:

var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
backgroundConfiguration.backgroundColorTransformer = UIConfigurationColorTransformer { _ in
    if cell.configurationState.isSelected || cell.configurationState.isHighlighted {
        .systemBlue.withAlphaComponent(0.7)
    } else {
        .systemBlue
    }
}
cell.backgroundConfiguration = backgroundConfiguration

However, if you push a view controller when you select the row, the row gets deselected, which is unfortunate, giving that I like to deselect rows in viewWillAppear(_:) to keep users more oriented.

There might be ways to circumvent this, but my custom color transformer might still differ from the default one in some other ways.

So how do I assign the default one to my cell?

PS I've also tried and didn't work cell.defaultBackgroundConfiguration().backgroundColorTransformer.

And using collection view controllers (and setting clearsSelectionOnViewWillAppear to false).

Here follows a more up to date version of the post, improved for clarity.

Since it looks like I can no longer edit nor delete my original question on this forum and I don't want to make a duplicate, it seems appropriate to me to link to a forum that lets you: https://stackoverflow.com/questions/78830896/setting-the-backgroundcolor-property-of-uibackgroundconfiguration-breaks-the-d

@Filippo02 My understanding of your question is that there is some confusion around what a color transformer does, and how configurations are used to update the appearance of a cell for different cell states.

For starters, a color transformer is just a simple function that takes in an input color and returns an output color (usually by modifying the RGBA values of the input color in some way).

When a cell is in the normal unselected state, and then becomes selected, the change in appearance happens because an entire new UIBackgroundConfiguration is generated and applied to the cell for that new UICellConfigurationState (which will include isSelected = true).

Sometimes, the difference between the original (unselected) and selected configurations is just a different color transformer applied, and the base color (e.g. the backgroundColor) stays the same. But other times, the color transformer doesn't change at all (it may not even exist), and it's just the base color changing. Both of these are perfectly valid, and the system default styling (i.e. the values that you get from UIBackgroundConfiguration when it is updated for different cell states) may use either or both approaches in different cases.

So in your code, when you set backgroundConfiguration.backgroundColor = .systemBlue, this doesn't affect the backgroundColorTransformer at all, but it does cause the backgroundColor property to become "locked" to the custom value you set. What this means is that when the cell automatically updates its backgroundConfiguration as it transitions to new states (like when it becomes selected), if the system default styling for that new state would use a different backgroundColor, you won't see any effect because the value you previously set has "locked" that property from automatic updates.

When you want to customize the appearance for certain states only, you need to perform those customizations in one of two places:

  1. Inside an override of the updateConfiguration(using:) method on your cell subclass, if you have one.
  2. Inside the configurationUpdateHandler closure (property you can set on the cell).

In the above example, you're just using UICollectionViewListCell (not a subclass), so you'd want to use option 2.

By putting your code that creates & assigns the cell configurations inside this configuration update method/closure, it ensures that your code will run again anytime the cell's state changes (which is passed in to the method/closure).

My guess is that this is what you were looking for when trying to write a custom color transformer -- think of color transformers as pure functions, which should not capture or depend on anything other than the input color.

When the cell's state changes, you want to set an entirely new backgroundConfiguration generated for the new state of the cell.

Hopefully that helps provide some clarity. I would also recommend revisiting the Modern Cell Configuration video from WWDC20 which also covers this in more detail: https://developer.apple.com/videos/play/wwdc2020/10027/

@Frameworks Engineer Thank you for your reply.

I highly encourage you to read my updated question on stack overflow, as I suggested doing in a previous reply.

In particular, the question has been made explicit and is also now clearly introduced by a header.

Could you please answer the question by modifying the provided code?

Playing around with configurationUpdateHandler didn't do it for me.

Problem

If you tap on the row, it doesn't look like it gets selected: the line backgroundConfiguration.backgroundColor = .systemBlue breaks the cell's default background color transformer.

Question

Given that my goal is to have my cell manifest its selection exactly like usual (meaning exactly as it would without the line backgroundConfiguration.backgroundColor = .systemBlue), that the details of how a cell usually does so are likely not public, that I would like to set a custom background color for my cell and that I would want to configure its appearance using configurations, since I seem to understand that that is the way to go from iOS 14 onwards, does anybody know how to achieve my goal by resetting something to whatever it was before I said backgroundConfiguration.backgroundColor = .systemBlue?

What I've tried and didn't work:

  1. Setting the collection view's delegate and specifying that you can select any row
  2. Setting the color transformer to .grayscale
  3. Setting the cell's backgroundConfiguration to UIBackgroundConfiguration.listGroupedCell().updated(for: cell.configurationState)
  4. Setting the color transformer to cell.defaultBackgroundConfiguration().backgroundColorTransformer
  5. Using collection view controllers (and setting collectionView.clearsSelectionOnViewWillAppear to false)
  6. Setting the cell's automaticallyUpdatesBackgroundConfiguration to false and then back to true
  7. Putting the cell's configuration code inside a configurationUpdateHandler
  8. Combinations of the approaches above
  9. Setting the color transformer to UIBackgroundConfiguration.listGroupedCell().backgroundColorTransformer and cell.backgroundConfiguration?.backgroundColorTransformer (they're both nil)

Workaround 1: use a custom color transformer

var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
backgroundConfiguration.backgroundColorTransformer = .init { _ in
    if cell.configurationState.isSelected || cell.configurationState.isHighlighted || cell.configurationState.isFocused {
        .systemRed
    } else {
        .systemBlue
    }
}
cell.backgroundConfiguration = backgroundConfiguration

Workaround 2: don't use a background configuration

You can set the cell's selectedBackgroundView, like so:

let v = UIView()
v.backgroundColor = .systemBlue
cell.selectedBackgroundView = v

You won't be able to use custom background content configurations though and might want to use background views instead:

var contentConfiguration = UIListContentConfiguration.cell()
contentConfiguration.text = "Hello"
cell.contentConfiguration = contentConfiguration

let v = UIView()
v.backgroundColor = .systemBlue
cell.backgroundView = v

let bv = UIView()
bv.backgroundColor = .systemRed
cell.selectedBackgroundView = bv

Consideration on the workarounds

Both workarounds seem to also not break this code, which deselects cells on viewWillAppear(_:) and was taken and slightly adapted from Apple's Modern Collection Views project (e.g. EmojiExplorerViewController.swift):

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    deselectSelectedItems(animated: animated)
}

func deselectSelectedItems(animated: Bool) {
    if let indexPath = collectionView.indexPathsForSelectedItems?.first {
        if let coordinator = transitionCoordinator {
            coordinator.animate(alongsideTransition: { [weak self] context in
                self?.collectionView.deselectItem(at: indexPath, animated: true)
            }) { [weak self] (context) in
                if context.isCancelled {
                    self?.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
                }
            }
        } else {
            collectionView.deselectItem(at: indexPath, animated: animated)
        }
    }
}

(Collection view controllers don't sport all of that logic out of the box, even though their clearsSelectionOnViewWillAppear property is true by default.)

Setting the `backgroundColor` property of UIBackgroundConfiguration breaks the default UIConfigurationColorTransformer
 
 
Q