I am trying to use the latest UICollectionViewCompositionalLayout to display paginated data using the UICollectionViewDiffableDataSource . I wanted to show a sticky header above the section that would stay on the top always even while loading the next page data from network .. I've noticed the sticky header is not working as expected and instead feels jerky while downloading data in the background and applying a new snapshot.. I was able to reproduce this issue with the sample app shared by Apple here
Here is the code to reproduce the issue:
func layout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(44))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 5
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(44)),
elementKind: sectionHeaderElementKind,
alignment: .top)
sectionHeader.pinToVisibleBounds = true
sectionHeader.zIndex = 2
section.boundarySupplementaryItems = [sectionHeader]
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
The above snippet provides the UICollectionViewCompositionalLayout with section header pinned to visible bounds at the top..
func setUpCollectionView() {
let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground
collectionView.register(ListCell.self, forCellWithReuseIdentifier: ListCell.reuseIdentifier)
collectionView.register(TitleSupplementaryView.self,
forSupplementaryViewOfKind: sectionHeaderElementKind,
withReuseIdentifier: TitleSupplementaryView.reuseIdentifier)
self.view.addSubview(collectionView)
collectionView.refreshControl = refreshControl
self.collectionView = collectionView
}
In the above code, I am adding the collection view to view controllers view.
func configureDataSource() {
guard let collectionView = self.collectionView else {
return
}
dataSource = UICollectionViewDiffableDataSource<String, Int>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
// Get a cell of the desired kind.
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: ListCell.reuseIdentifier,
for: indexPath) as? ListCell else { fatalError("Cannot create new cell") }
// Populate the cell with our item description.
cell.label.text = "\(indexPath.section),\(indexPath.item)"
if self.canLoadNextPage(indexpath: indexPath) {
self.getNextPage()
}
// Return the cell.
return cell
}
dataSource?.supplementaryViewProvider = {
(collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? in
// Get a supplementary view of the desired kind.
guard let headerFooter = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: TitleSupplementaryView.reuseIdentifier,
for: indexPath) as? TitleSupplementaryView else { fatalError("Cannot create new header") }
headerFooter.label.text = sectionHeader
headerFooter.backgroundColor = .lightGray
headerFooter.layer.borderColor = UIColor.black.cgColor
headerFooter.layer.borderWidth = 1.0
// Return the view.
return headerFooter
}
var snapshot = NSDiffableDataSourceSnapshot<String, Int>()
snapshot.appendSections([section])
items = Array(currentOffset..<currentOffset + 2*itemsPerPage)
currentOffset += 2*itemsPerPage
snapshot.appendItems(items)
serialQueue.async { [weak self] in
self?.dataSource?.apply(snapshot, animatingDifferences: true)
}
}
In the above code, I am creating a diffable datasource that builds the cells and header view.. It also initializes the collection view with a snapshot of initial data.. The snapshots are always applied in a background serial queue.
When the content offset is closer towards the end, I am getting the next page of data and applying a new snapshot as you can see here..
func canLoadNextPage(indexpath: IndexPath) -> Bool {
guard (indexpath.item + 5) > currentOffset else {
return false
}
return true
}
func getNextPage() {
fetchQueue.asyncAfter(deadline: .now() + 0.5) {
var snapshot = NSDiffableDataSourceSnapshot<String, Int>()
snapshot.appendSections([section])
self.items.append(contentsOf: Array(self.currentOffset..<self.currentOffset + itemsPerPage))
snapshot.appendItems(self.items)
self.currentOffset += itemsPerPage
self.serialQueue.async { [weak self] in
self?.dataSource?.apply(snapshot, animatingDifferences: true)
}
}
}
The similar behavior is also observed with Pull To Refresh actions..
Another thing i've noticed, this doesn't happen when you add data immediately without delay and switching between threads.
Appreciate any help..
pinToVisibleBounds
tofalse
and applying snapshot without animation didn't work for me – Sheree