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