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.)
backgroundConfiguration.backgroundColorTransformer = cell.defaultBackgroundConfiguration().backgroundColorTransformer
(just added) seems like assigning a default color transformer (which is not even nil), just like you can sayvar contentConfiguration = cell.defaultContentConfiguration()
. As per your second comment, I actually meantviewWillAppear(_:)
, 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. – FeatherweightUICollectionViewController
subclass which hasclearsSelectionOnViewWillAppear
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 you – FeatherweightA
be “make the cell in the shown sample app update its background color exactly like a cell defined by an empty cell registration would”. – FeatherweightA
without removing the linebackgroundConfiguration.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 toA
are not public, I would like to know if anybody knows how toA
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. – FeatherweightUICollectionViewListCell
. 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 cell – FeatherweightA
be “make the cell in the shown sample app update its background color exactly like aUICollectionViewListCell
described by an empty cell registration would” – Featherweight