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:
- Setting the collection view's delegate and specifying that you can select any row
- Setting the color transformer to
.grayscale
- Setting the cell's
backgroundConfiguration
toUIBackgroundConfiguration.listGroupedCell().updated(for: cell.configurationState)
- Setting the color transformer to
cell.defaultBackgroundConfiguration().backgroundColorTransformer
- Using collection view controllers (and setting
collectionView.clearsSelectionOnViewWillAppear
tofalse
) - Setting the cell's
automaticallyUpdatesBackgroundConfiguration
tofalse
and then back totrue
- Putting the cell's configuration code inside a
configurationUpdateHandler
- Combinations of the approaches above
- Setting the color transformer to
UIBackgroundConfiguration.listGroupedCell().backgroundColorTransformer
andcell.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.)
@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
}