2

On iOS 10.0, UICollectionView pre-fetches cells by default. This leads to cells that are prepared for being shown on screen, but are hidden. This question describes it really well.

The following code will successfully deselect an index path when its cell is either visible or does not exist at all. If the cell exists and is hidden, the index path will be deselected, but the cell becomes stuck in the selected state until it is reused.

collectionView!.deselectItem(at: indexPath, animated: false)

This problem does not exits on iOS 9 or when pre-fetching is disabled with isPrefetchingEnabled = false on iOS 10.0.

Is this a bug in UICollectionView or am I misunderstanding how deselectItem is supposed to work?


Here is the full code of a UICollectionViewController subclass that demonstrates this behaviour with the following steps:

  • Tap on a cell, so that it becomes selected (red)
  • Scroll the cell slightly off-screen
  • Tap the "Deselect Cell" button
  • Scroll the cell back on screen
  • Observe how it still looks selected
  • Tap on another cell
  • Observe how both cells look selected
  • Scroll the first cell far off-screen and back again
  • Observe how the first cell finally does not look selected


import UIKit

private let reuseIdentifier = "Cell"

class CollectionViewController: UICollectionViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.collectionView!.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)

        let button = UIButton(frame: CGRect(x: 10, y: 30, width: 360, height: 44))
        button.backgroundColor = #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1)
        button.setTitleColor(#colorLiteral(red: 1, green: 1, blue: 1, alpha: 1), for: .normal)
        button.setTitleColor(#colorLiteral(red: 0.05882352963, green: 0.180392161, blue: 0.2470588237, alpha: 1), for: .highlighted)
        button.setTitle("Deselect Cell", for: .normal)
        button.addTarget(self, action: #selector(CollectionViewController.buttonPress), for: .touchUpInside)
        view.addSubview(button)
    }

    func buttonPress() {
        for indexPath in collectionView!.indexPathsForSelectedItems ?? [IndexPath]() {

            let cell = collectionView!.cellForItem(at: indexPath)
            NSLog("Deselecting indexPath: %@, cell: %@", indexPath.description, cell?.frame.debugDescription ?? "not visible")

            collectionView!.deselectItem(at: indexPath, animated: false)
        }
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 300
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
        cell.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
        cell.selectedBackgroundView = UIView(frame: cell.bounds)
        cell.selectedBackgroundView!.backgroundColor = #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1)
        return cell
    }
}
Community
  • 1
  • 1
Sebastian Kirsche
  • 861
  • 2
  • 19
  • 36

1 Answers1

1

As far as I can tell this is a bug in UICollectionView, and I've opened a Radar myself. You might want to do so as well to apply more pressure.

Even before prefetching, collection view didn't bother deselecting cells that weren't visible. Even cellForItemAtIndexPath: states that it returns nil for cells that are not visible.

But before prefetching, as soon as a cell left the content view it was added to the reuse pool, and when you scrolled back you got a reused cell that had its selected state reset.

Now, that cell remains loaded and wont reset until it's reused, by scrolling further away, beyond the prefetching area.

To fix this you can either set prefetchingEnabled to NO and lose its benefits, or update selection state whenever a cell appears -

- (void)collectionView:(UICollectionView *)collectionView
       willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
  if (!cell.isSelected) {
    return;
  }

  if ([[collectionView indexPathsForSelectedItems] containsObject:indexPath]) {
    return;
  }

  cell.selected = NO;
}

This doesn't seem to diminish performance.

Barak Yoresh
  • 279
  • 2
  • 4
  • This code will fix the issue where cells are shown selected when they're not supposed to be, but beware that there's another possibility, that cells will be shown deselected when they're supposed to be selected. – Theme Aug 07 '17 at 17:42
  • @user3246268 That's not an issue in this particular case. The problem is the cells in the prefetch area - but a cell that was selected will remain selected, since the system doesn't call `prepareForReuse` for those cells. For all purposes they are cells that are still visible, except for the fact that the cells wont be deselected if you programatically deselect them while they aren't visible - in which case the system wrongfully assumes there's no point to do that, since they will be `preparedForReuse` anyway before appearing. – Barak Yoresh Aug 10 '17 at 04:55