21

I have a UICollectionView using a standard UICollectionViewFlowLayout with an estimatedItemSizeset to UICollectionViewFlowLayoutAutomaticSize.

Once I call collectionView.reloadData() the scroll position of the view resets to CGPoint(x: 0, y: 0). I know that .reloadData() should only be used as a last resort, it is necessary in my scenario though. I have found out so far that it is probably caused by not returning an item size through collectionView(_:layout:sizeForItemAt:). (Any solutions for that analogous to UITableView’s tableView:estimatedHeightForRowAtIndexPath: like in this StackOverflow answer would be highly appreciated).

Aaron
  • 269
  • 3
  • 11
  • Can you add more details of what you're trying to achieve? I can't seem to get a visual of your end result. Sounds like you could possibly override `collectionViewContentSize` using a custom `UICollectionViewFlowLayout` to stop the reset off the offset. – luke Oct 22 '17 at 09:40
  • 1
    Do u mean that after calling reloadData(), the collection view scrolls to top ?? – Maddiee Oct 24 '17 at 19:09
  • Maybe you try by calling "invalidateLayout" and then reloading. Because reloading shouldn't be considered as a last resort. How else would you reload the entire data. – Maddiee Oct 24 '17 at 19:21
  • reloadData() reloads the interface also, so maybe storing the position your collection is at and setting it manually after the reload? – Bruno Fulber Wide Oct 26 '17 at 15:06
  • Hi OP. Did you solve this? I am currently having this problem. – Rakesha Shastri Jan 08 '19 at 12:39
  • Any workaround guys? i have the same problems. – axunic Oct 04 '19 at 13:07

5 Answers5

3

in my case I fixed it so: here I detect if my cell is fully visible, and if it's not so - move to it. (without "jumping")

 let cellRect = collectionView.convert(cell.frame, to: collectionView.superview)
      if !collectionView.frame.contains(cellRect) {
        collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
        return collectionView.reloadData()
      }

here, I hold my contentOffset:

   let currentContentOffSet = collectionView.contentOffset
         if #available(iOS 12.0, *) {
            UIView.animate(withDuration: 0) {
               collectionView.reloadData()
               collectionView.performBatchUpdates(nil, completion: { _ in
                  collectionView.contentOffset = currentContentOffSet
               })
            }
         } else {
            UIView.animate(withDuration: 0) {
               collectionView.reloadItems(at: [self.lastSelectedIndexPath, indexPath])
               collectionView.contentOffset = currentContentOffSet
            }
         }

it helps move to cells if cells became visible recently (or already visible) and I tap to the cell. Also without "jumping" (without resetting contentOffset)

Also you can try this in collectionViewController:

  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
      guard let cell = collectionView.cellForItem(at: indexPath) else { return }
      if lastSelectedIndexPath != indexPath {
         let cellRect = collectionView.convert(cell.frame, to: collectionView.superview)
         if !collectionView.frame.contains(cellRect) {
            collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
         } else {
            collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .init())
         }
         collectionView.deselectItem(at: lastSelectedIndexPath, animated: false)
         lastSelectedIndexPath = indexPath
      }
   }

and this in collectionViewCellModel:

  override var isSelected: Bool {
      didSet {
         switch self.isSelected {
         case false:

         case true:

         }
      }
   }
ArturRuz
  • 49
  • 1
  • 5
  • +1. using `performBatchUpdates` completion handler was the only reliable way which worked for me to set `contentOffset` immediately after reloading data. – markckim Nov 18 '22 at 07:06
3

I had the same issue and solved it by subclassing the collection view, override reloadData to temporary store the current contentOffset and then listening to changes to the contentOffset and changing it back if i have anything stored in my temporary offset variable

class MyCollectionView : UICollectionView {
    deinit {
        removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset))
    }

    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    private func setup() {
        self.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: [.old, .new], context: nil)
    }

    private var temporaryOffsetOverride : CGPoint?
    override func reloadData() {
        if let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout, flowLayout.estimatedItemSize == UICollectionViewFlowLayout.automaticSize {
            temporaryOffsetOverride = contentOffset
        }
        super.reloadData()
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == #keyPath(UIScrollView.contentOffset) {
            if let offset = temporaryOffsetOverride {
                temporaryOffsetOverride = nil
                self.setContentOffset(offset, animated: false)
            }
        }
    }
}
SveinnV
  • 184
  • 4
1

Reloading sections of UICollectionView worked for me.

UIView.performWithoutAnimation {self.collStages.reloadSections([0])}
collStages.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: true)
Dhaval Bhimani
  • 980
  • 1
  • 13
  • 24
0

reloadData reloads all cells in your UICollectionView so if you have any reference to cells it will point to object that is not exist, and what's more if it's strong reference it may cause a retain cycle. So:

You should not do it!

But...

It's hard to imagine the scenario where reloadData is needed and it cannot be replaced by inserting and deleting objects.

So what you may want to use is reload single row or maybe insert items that it change in your data model. You didn't post what your problem is with details, but you can do any of this actions mentioned earlier like:

        collectionView.performBatchUpdates({
            collectionView.insertItems(at: <#T##[IndexPath]#>)
            collectionView.deleteItems(at: <#T##[IndexPath]#>)
            collectionView.reloadItems(at: <#T##[IndexPath]#>)
        }, completion: { (<#Bool#>) in
            <#code#>
        })

But make sure you'll edit your data model first. So:

var dataModel: [String] = ["0","1","2"]   //Data model for Collection View
collectionView.reloadData() // In that point you will have 3 cells visible on the sceen
.
.
.
dataModel.removeFirst() // now your data model is ["1","2"]
collectionView.performBatchUpdates({
    collectionView.deleteItems(at: [IndexPath(row: 0, section:0)])
}, completion: { success in 
    //some check consistency code
})

And with that, your collectionview will remain the same, and no other change will be made besides this first row.

Jakub
  • 13,712
  • 17
  • 82
  • 139
-1

Not sure if it might work, but you can give a try :

    let currentOffset = self.collectionView.contentOffset
    self.collectionView.reloadData()
    self.collectionView.setContentOffset(currentOffset, animated: false)

Do lemme know. Thanks :)

Maddiee
  • 288
  • 2
  • 11