'Stop Diffable Data Source scrolling to top after refresh

How can I stop a diffable data source scrolling the view to the top after applying the snapshot. I currently have this...

    fileprivate func configureDataSource() {
        self.datasource = UICollectionViewDiffableDataSource<Section, PostDetail>(collectionView: self.collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, userComment: PostDetail) -> UICollectionViewCell? in
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PostDetailCell.reuseIdentifier, for: indexPath) as? PostDetailCell else { fatalError("Cannot create cell")}
            
            cell.user = self.user
            cell.postDetail = userComment
            cell.likeCommentDelegate = self
            return cell
        }
        
        var snapshot = NSDiffableDataSourceSnapshot<Section, PostDetail>()
        snapshot.appendSections([.main])
        snapshot.appendItems(self.userComments)
        self.datasource.apply(snapshot, animatingDifferences: true)
    }

    fileprivate func applySnapshot() {

        //let contentOffset = self.collectionView.contentOffset
        var snapshot = NSDiffableDataSourceSnapshot<Section, PostDetail>()
        snapshot.appendSections([.main])
        snapshot.appendItems(self.userComments)
        self.datasource.apply(snapshot, animatingDifferences: false)
        //self.collectionView.contentOffset = contentOffset
    }

store the offset, then reapply it. Sometimes it works perfectly and sometimes the view jumps. Is there a better way of doing this?



Solution 1:[1]

The source of this problem is probably your Item identifier type - the UserComment.

Diffable data source uses the hash of your item identifier type to detect if it is a new instance or an old one which is represented currently. If you implement Hashable protocol manually, and you use a UUID which is generated whenever a new instance of the type is initialized, this misguides the Diffable data source and tells it this is a new instance of item identifier. So the previous ones must be deleted and the new ones should be represented. This causes the table or collection view to scroll after applying snapshot. To solve that replace the uuid with one of the properties of the type that you know is unique or more generally use a technique to generate the same hash value for identical instances.

So to summarize, the general idea is to pass instances of the item identifiers with the same hash values to the snapshot to tell the Diffable data source that these items are not new and there is no need to delete previous ones and insert these ones. In this case you will not encounter unnecessary scrolls.

Solution 2:[2]

First up: in most cases @Amirrezas answer will be the correct reason for the problem. In my case it was not the item, but the section identifier that caused the problem. That was Hashable and Identifiable with correct values, but it was a class, and therefore the hash functions were never called. Took me a while to spot that problem. Changing to a struct (and therefore adopting some things ;) ) helped in my case.

For reference here's a link to the topic on the Apple-Dev forums: https://developer.apple.com/forums/thread/657499

Hope my answer helps somebody :)

Solution 3:[3]

Starting from iOS 15

dataSource.applySnapshotUsingReloadData(snapshot, completion: nil)

Resets the UI to reflect the state of the data in the snapshot without computing a diff or animating the changes

Solution 4:[4]

You'd think that any of these methods would work:

https://developer.apple.com/documentation/uikit/uicollectionviewdelegate/1618007-collectionview https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617724-targetcontentoffset

But (in my case) they did not. You might get more mileage out of them, I am doing some crazy stuff with a custom UICollectionViewCompositionalLayout

What I did get to work is manually setting the offset in my custom layout class:

override func finalizeCollectionViewUpdates() {
    if let offset = collectionView?.contentOffset {
        collectionView?.contentOffset = targetContentOffset(forProposedContentOffset: offset)
    }
    super.finalizeCollectionViewUpdates()
}

where I have targetContentOffset also overridden and defined (I tried that first, didn't work, figured it was cleanest to just use that here. I suspect if you define targetContentOffset on the delegate without overriding it in the layout the above will also work, but you already need a custom layout to get this far so it's all the same.)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Georg
Solution 3 mattyU
Solution 4 nickneedsaname