Setting the `backgroundColor` property of UIBackgroundConfiguration breaks the default UIConfigurationColorTransformer
Asked Answered
F

0

0

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

Featherweight answered 4/8 at 11:7 Comment(7)
@matt backgroundConfiguration.backgroundColorTransformer = cell.defaultBackgroundConfiguration().backgroundColorTransformer (just added) seems like assigning a default color transformer (which is not even nil), just like you can say var contentConfiguration = cell.defaultContentConfiguration(). As per your second comment, I actually meant viewWillAppear(_:), you can find an example of what I mean in EmojiExplorerViewController of Apple's Modern Collection Views project. clearsSelectionOnViewWillAppear doesn't implement all of that logic automatically.Featherweight
Also the view controllers in my real project all inherit from a UICollectionViewController subclass which has clearsSelectionOnViewWillAppear set to false and handles item deselection as show in Apple's project. My post now mentions that I've already tried using collection view controllers and setting the property to false, thank youFeatherweight
@matt I would like to know how if there's a way to successfully assign the default background color transformer, given that there might be a working approach that I haven't tried yetFeatherweight
@matt [1/2] Ah, I thought in that case you would have had to say you’re leaving the background color transformer as the default. Anyways, let A be “make the cell in the shown sample app update its background color exactly like a cell defined by an empty cell registration would”.Featherweight
@matt [2/2] My goal is to A without removing the line backgroundConfiguration.backgroundColor = .systemBlue, given that that’s part of how I seem to understand you should set a cell’s background color to .systemBlue from iOS 14 onwards. Suspecting that the details of how to A are not public, I would like to know if anybody knows how to A by resetting something (possibly the cell’s background color transformer, given that that's what I seem to understand you should tweak to update a cell's background color from iOS 14+) to whatever it was before I set the cell's background color.Featherweight
You can set the cell's color inside it (inside its handler, which is its only parameter, at least in the reference code), which doesn't happen if it's empty. You may understand that I did not summarize my whole post inside those 2 comments, and that I just tried reformulating the question since you said you did not grasp it, and that I gave for granted that the cell is of type UICollectionViewListCell. You're right a cell isn't defined by its registration though, I was just looking at my code and saw that emptying the registration was the fastest way to reset the cellFeatherweight
So let A be “make the cell in the shown sample app update its background color exactly like a UICollectionViewListCell described by an empty cell registration would”Featherweight

© 2022 - 2024 — McMap. All rights reserved.