UICollectionViewCompositionalLayout's sticky header is jerky
Asked Answered
O

0

6

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

enter image description here

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

Ovovitellin answered 23/4, 2020 at 1:6 Comment(5)
Figured this issue is not happening when snapshot is applied without animatingOvovitellin
any updates on that? I'm experiencing the same issue.. driving me crazy!Hafler
Applying the snapshot without animation fixes the issue.. self?.dataSource?.apply(snapshot, animatingDifferences: false)Ovovitellin
This is so frustrating. Same issue on iOS 14.2, for now I had to set pinToVisibleBounds to false and applying snapshot without animation didn't work for meSheree
UICollectionViewCompositionalLayout and UICollectionViewDiffableDataSource although are extremely useful, they have horrible number of issues and got even worse with iOS14.Ovovitellin

© 2022 - 2024 — McMap. All rights reserved.