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

Sample app

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

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.)

Answered by DTS Engineer in 799963022

@Filippo02 Your first workaround using the backgroundColorTransformer is the actual solution you need.

  • Additionally, you can set a nil background color to use the view's tint color.

var backgroundConfig = UIBackgroundConfiguration.listPlainCell()


// Set a nil background color to use the view's tint color. 
backgroundConfig.backgroundColor = nil 


cell.backgroundConfiguration = backgroundConfig

If you want additional customization beyond the system default values, you can choose to manually update the background configuration by overriding the view’s updateConfiguration(using:) method.

override func updateConfiguration(using state: UIConfigurationState) {
    // Get the system default background configuration for a plain style list cell in the current state. 
    var backgroundConfig = UIBackgroundConfiguration.listPlainCell().updated(for: state) 


    // Customize the background color to use the tint color when the cell is highlighted or selected. 
     if state.isHighlighted || state.isSelected { 
        backgroundConfig.backgroundColor = nil 
     } 


    // Apply the background configuration to the cell. 
    self.backgroundConfiguration = backgroundConfig 
} 

@Filippo02 Your first workaround using the backgroundColorTransformer is the actual solution you need.

  • Additionally, you can set a nil background color to use the view's tint color.

var backgroundConfig = UIBackgroundConfiguration.listPlainCell()


// Set a nil background color to use the view's tint color. 
backgroundConfig.backgroundColor = nil 


cell.backgroundConfiguration = backgroundConfig

If you want additional customization beyond the system default values, you can choose to manually update the background configuration by overriding the view’s updateConfiguration(using:) method.

override func updateConfiguration(using state: UIConfigurationState) {
    // Get the system default background configuration for a plain style list cell in the current state. 
    var backgroundConfig = UIBackgroundConfiguration.listPlainCell().updated(for: state) 


    // Customize the background color to use the tint color when the cell is highlighted or selected. 
     if state.isHighlighted || state.isSelected { 
        backgroundConfig.backgroundColor = nil 
     } 


    // Apply the background configuration to the cell. 
    self.backgroundConfiguration = backgroundConfig 
} 
Setting the `backgroundColor` property of UIBackgroundConfiguration breaks the default UIConfigurationColorTransformer
 
 
Q