Stop Diffable Data Source scrolling to top after refresh
Asked Answered
T

4

7

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?

Turbofan answered 7/11, 2020 at 0:28 Comment(4)
Unless you are initially populating the collection view (first time only), you should not make a new snapshot and apply it. You should get the snapshot from the data source, modify it, and apply it. And do not attempt to play with the content offset at all. It will just update, in place.Tablespoon
Hi, thanks for commenting, I have updated my code to show you exactly what I am doing. So I initialise it, then apply the snapshot. This was based off the apple examples I downloaded, is this not correct?Turbofan
By retrieving the current snapshot, modifying it and applying it in case of changes we will have to manage two sources of truth as we did with UITableViewDataSource. By this way we will lose the most important benefit of Diffable Data Source. So this is not a good approach.Taraxacum
agreed with @Amirreza, I think the purpose of Diffable data source, at least from how it was marketed in WWDC, is that you don't need to worry about managing state. the framework should be able to compare two data sources and provide animations out the box. 'm running into this same exact problem. additionally, I'm passing in the same custom hashable identifiers for cells that shouldn't be animating, but they are. I've used IGListKit before so I'm aware this should not be the expected behavior.Loris
T
12

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.

Taraxacum answered 12/11, 2020 at 9:35 Comment(2)
Thank you very Amirreza, that is exactly the issue. I have sent you a message on LinkedIn.Turbofan
Finally, this helped me! In my case, I was using a model, that had a property of a struct with an identifier that was default set to UUID().uuidString. I updated my code, explicitly setting these identifiers to unique values, which resolved the tableView jumping for me.Stroganoff
M
3

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

Milkandwater answered 20/10, 2021 at 14:13 Comment(0)
K
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

Kevon answered 20/3, 2022 at 17:33 Comment(0)
C
1

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

Cychosz answered 22/4, 2022 at 20:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.