The following UIKit app uses a collection view with compositional layout (list layout) and diffable data source.
If you run it and then tap on the view controller's right bar button item, the footerText
property will change.
How may I update the collection view's footer to display the updated footerText
, without interfering with the collection view's animations, and possibly also in a way that is performant and recommended by Apple?
By "collection view's animations" I mean any animation of the collection view or of any of its subviews.
So, for instance, I wouldn't want to reconfigure the contents of the supplementary view by reloading the collection view's data.
You can think of this problem as a problem such as how to keep compositional layout badges up to date without giving up on animations.
class ViewController: UIViewController {
var collectionView: UICollectionView!
var footerText = "Initial footer text"
var dataSource: UICollectionViewDiffableDataSource<Section, String>!
var snapshot: NSDiffableDataSourceSnapshot<Section, String> {
var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
snapshot.appendSections(Section.allCases)
snapshot.appendItems(["A", "a"], toSection: .first)
return snapshot
}
enum Section: CaseIterable {
case first
}
override func viewDidLoad() {
super.viewDidLoad()
configureHierarchy()
configureDataSource()
}
func configureHierarchy() {
navigationItem.rightBarButtonItem = .init(title: "Change footer text", style: .plain, target: self, action: #selector(changeFooterText))
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
view.addSubview(collectionView)
collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
@objc func changeFooterText() {
footerText = "Secondary footer text"
}
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
var contentConfiguration = UIListContentConfiguration.cell()
contentConfiguration.text = itemIdentifier
cell.contentConfiguration = contentConfiguration
}
dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
}
configureSupplementaryViewProvider()
dataSource.apply(self.snapshot)
}
func configureSupplementaryViewProvider() {
let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { headerView, elementKind, indexPath in
var contentConfiguration = UIListContentConfiguration.cell()
contentConfiguration.text = "Header \(indexPath.section)"
headerView.contentConfiguration = contentConfiguration
}
let footerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionFooter) { [weak self] headerView, elementKind, indexPath in
guard let self else { return }
var contentConfiguration = UIListContentConfiguration.cell()
contentConfiguration.text = self.footerText
headerView.contentConfiguration = contentConfiguration
}
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
if kind == UICollectionView.elementKindSectionHeader {
collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
} else if kind == UICollectionView.elementKindSectionFooter {
collectionView.dequeueConfiguredReusableSupplementary(using: footerRegistration, for: indexPath)
} else {
nil
}
}
}
func createLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { section, layoutEnvironment in
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
config.headerMode = .supplementary
config.footerMode = .supplementary
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
}
}
}
What I've tried to do in footerText
's didSet:
var footerText = "Initial footer text" {
didSet {
configureSupplementaryViewProvider()
}
}
var footerText = "Initial footer text" {
didSet {
configureSupplementaryViewProvider()
dataSource.apply(self.snapshot)
}
}
var footerText = "Initial footer text" {
didSet {
configureSupplementaryViewProvider()
dataSource.apply(self.snapshot, animatingDifferences: true)
var snapshot = dataSource.snapshot()
snapshot.reconfigureItems(snapshot.itemIdentifiers)
dataSource.apply(snapshot, animatingDifferences: false)
}
}
To directly update a supplementaryView
you need to know its kind and section.
With your example, you could add a following didSet
code to a footerText:
var footerText = "Initial footer text" {
didSet {
guard let footer = collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: IndexPath(row: 0, section: 0)) as? UICollectionViewListCell else { return }
var contentConfiguration = UIListContentConfiguration.cell()
contentConfiguration.text = self.footerText
footer.contentConfiguration = contentConfiguration
}
}
This way each time you change footerText, your footer will be updated.
You could also move contentConfiguration update to a separate fuction, I've just copied it from your configureSupplementaryViewProvider