user784637
user784637

Reputation: 16092

Pull to refresh for UICollectionView in ViewController jumps when applying NSDiffableDataSourceSnapshot

I've added a UICollectionView as a subview to my ViewController. I've also set a UIRefreshControl for the UICollectionView which applies a NSDiffableDataSourceSnapshot when I pull down to refresh. The animation jumps when applying the snapshot (note this happens even when the navigator bar title isn't large.)

enter image description here

Here are the relevant snippets of code

var collectionView: UICollectionView! = nil
var dataSource: UICollectionViewDiffableDataSource<Section, Item>! = nil

override func viewDidLoad() {
    super.viewDidLoad()
    
    let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: 
    generateLayout())
    view.addSubview(collectionView)
    collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
    collectionView.backgroundColor = .systemGroupedBackground
    self.collectionView = collectionView
    collectionView.delegate = self

    let refreshControl = UIRefreshControl()
    refreshControl.addTarget(self, action: #selector(doSomething), for: .valueChanged)
    collectionView.refreshControl = refreshControl
}
    
@objc func doSomething(refreshControl: UIRefreshControl) {
    DispatchQueue.main.async {
        var dataSourceSnapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        // add data
        dataSource.apply(dataSourceSnapshot)
    }
    refreshControl.endRefreshing()
}

Are there any changes I can make to apply the snapshot without the sudden jump?

Upvotes: 3

Views: 1970

Answers (1)

storoj
storoj

Reputation: 1867

Unfortunately Apple Engineers can not properly solve that issue since iOS 6.

Long story short: you don't want to endRefreshing while your scroll view isTracking. UIRefreshControl modifies contentInset and contentOffset of the scrollView, and it always caused jumps like that.

If you have a network request state anywhere, or any other flag indicating that the loading is in progress you could simply do:

@objc func doSomething(refreshControl: UIRefreshControl) {
    loading = true
    DispatchQueue.main.async {
        var dataSourceSnapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        // add data
        dataSourceSnapshot.appendSections([.test])
        dataSourceSnapshot.appendItems([.test])
        self.dataSource.apply(dataSourceSnapshot)
        self.loading = false
        
    }
}

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    if !loading && collectionView.refreshControl!.isRefreshing {
        collectionView.refreshControl!.endRefreshing()
    }
}

I don't quite like loading flags like that, but that's just an example. Hopefully you have better source of truth indicating if some "work" is still in progress.

I guess there might be a nicer way of solving it (by patching classes in runtime), but it usually takes time to investigate. I wonder if that solution satisfies your needs.

Upvotes: 2

Related Questions