I have a horizontal UICollectionView
with several sections, each containing several cells as follows:
Section 0: one cell to cancel selection.
Section 1: recently selected items.
Section 2 and beyond: each section has several items that can be selected.
When a cell from section 2 or later is selected, a copy of that item is inserted to the start section 1 in the data source, and I want to reload section 1 to reflect the updated recent items. But, I want to preserve the scroll position. I've tried:
[collectionView reloadSections:setWithIndex1]
and[collectionView reloadItemsAtIndexPaths:arrayWithAllSection1IndexPaths]
I've tried using [collectionView performBatchUpdates:]
, but everything I've tried makes the scroll offset reset to the beginning of the collection view. I've tried a sanity check by starting a fresh app with a basic collection view and reloading a section using reloadSections
, and it has the desired behavior of not resetting the scroll offset. But doing the same in my existing codebase does, undesirably, reset the offset.
I've poured over my collectionView
-related code looking for reloadData
's, setContentOffsets
's, and similar things, but for the life of me I can't find what's causing it. Is there anything I'm missing that could be resetting the scroll position after an update?
If you are OK with doing it without any animation I would do as follows:
UIView.setAnimationsEnabled(false)
let selectedCell = collectionView.cellForItem(at: indexPath)
let visibleRect = collectionView.convert(collectionView.bounds, to: selectedCell)
selectedObjects.insert(allObjects[indexPath.item], at: 0)
collectionView.performBatchUpdates({
// INSERTING NEW ITEM
let indexPathForNewItem = IndexPath(item: 0, section: 1)
collectionView.insertItems(at: [indexPathForNewItem])
}) { (finished) in
// GETTING NEW VISIBLE RECT FOR SELECTED CELL
let updatedVisibleRect = collectionView.convert(collectionView.bounds, to: selectedCell)
// UPDATING COLLECTION VIEW CONTENT OFFSET
var contentOffset = collectionView.contentOffset
contentOffset.x = contentOffset.x + (visibleRect.origin.x - updatedVisibleRect.origin.x)
collectionView.contentOffset = contentOffset
}
UIView.setAnimationsEnabled(true)
I tried it on a simple collection view adjusted to the behaviour you described. Here's the whole implementation (collecionView is in the storyboard, so if you want to give my solution a test, don't forget to connect the outlet.)
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var collectionView: UICollectionView!
let reuseIdentifier = "cell.reuseIdentifier"
var allObjects: [UIColor] = [.red, .yellow, .orange, .purple, .blue]
var selectedObjects: [UIColor] = []
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: self.reuseIdentifier)
}
}
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
switch section {
case 0: return 1
case 1: return selectedObjects.count
case 2: return allObjects.count
default: return 0
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.reuseIdentifier, for: indexPath)
switch indexPath.section {
case 0: cell.contentView.backgroundColor = .black
case 1: cell.contentView.backgroundColor = selectedObjects[indexPath.item]
case 2: cell.contentView.backgroundColor = allObjects[indexPath.item]
default: break
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 150, height: collectionView.frame.size.height)
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: false)
switch indexPath.section {
case 0:
self.selectedObjects.removeAll()
collectionView.reloadData()
case 2:
if selectedObjects.contains(allObjects[indexPath.item]) {
break
} else {
// SOLUTION //
UIView.setAnimationsEnabled(false)
let selectedCell = collectionView.cellForItem(at: indexPath)
let visibleRect = collectionView.convert(collectionView.bounds, to: selectedCell)
selectedObjects.insert(allObjects[indexPath.item], at: 0)
collectionView.performBatchUpdates({
let indexPathForNewItem = IndexPath(item: 0, section: 1)
collectionView.insertItems(at: [indexPathForNewItem])
}) { (finished) in
let updatedVisibleRect = collectionView.convert(collectionView.bounds, to: selectedCell)
var contentOffset = collectionView.contentOffset
contentOffset.x = contentOffset.x + (visibleRect.origin.x - updatedVisibleRect.origin.x)
collectionView.contentOffset = contentOffset
}
UIView.setAnimationsEnabled(true)
// END OF SOLUTION //
}
default:
break
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)
}
}
EDIT
I just also tried replacing
let indexPathForNewItem = IndexPath(item: 0, section: 1)
collectionView.insertItems(at: [indexPathForNewItem])
with
collectionView.reloadSections(IndexSet(integer: 1))
and it also works just fine, without any flickering, so it's up to you which is more convenient for you.
IMPORTANT!
If you want to preserve UICollectionView
scroll position when you fetch new items, it is better to insert the new items instead of reloading the entire section (or maybe the entire list!)..
But the trick here is to set the size of each item (or cell) to avoid jumping behavior (or flickers):
Inherit your ViewController
from UICollectionViewDelegateFlowLayout
and implement the sizeForItemAt
class myViewController: UIViewController, UICollectionViewDataSource,
UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
// Your core functions goes here
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: ITEM_WIDTH, height: ITEM_HEIGHT)
}
}
Insert the items:
for i in dictionary {
items.append(i)
collectionView.performBatchUpdates({
collectionView.insertItems(at: [IndexPath(item: items.count - 1, section: 0)])
}, completion: { [] (_) in
})
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With