iOS 14 Crash when Reordering Self-Sizing Collection View Cells

On an app I'm working on, I've run into a problem where trying to reorder the cells in a collection view causes the app to crash with EXC_BAD_ACCESS. Below is a very simplified form of that logic I wrote to try and pin down the cause:

Code Block swift
import UIKit
class ViewController: UIViewController {
   
  var colors: [UIColor] = [.blue, .green, .red, .brown]
   
  @IBOutlet weak var collectionView: UICollectionView!
  @IBOutlet weak var flowLayout: UICollectionViewFlowLayout!
   
  override func viewDidLoad() {
    super.viewDidLoad()
     
    self.collectionView.delegate = self
    self.collectionView.dataSource = self
    self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
   
    let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
    self.collectionView.addGestureRecognizer(longPressGesture)
      
    flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    self.collectionView.reloadData()
  }
  func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
    if let item = coordinator.items.first,
      let sourceIndexPath = item.sourceIndexPath {
       
      collectionView.performBatchUpdates({
        self.colors.remove(at: sourceIndexPath.item)
        self.colors.insert(item.dragItem.localObject as! UIColor, at: destinationIndexPath.item)
         
        collectionView.deleteItems(at: [sourceIndexPath])
        collectionView.insertItems(at: [destinationIndexPath])
         
      }, completion: nil)
      coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
    }
       
  }
   
  @objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
    switch gesture.state {
    case .began:
      guard let selectedIndexPath = self.collectionView.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
        break
      }
      self.collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
    case .changed:
      self.collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: self.collectionView))    
    case .ended:
      self.collectionView.endInteractiveMovement()
    default:
      self.collectionView.cancelInteractiveMovement()
    }
  }
}
extension ViewController: UICollectionViewDelegateFlowLayout {
 func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: 50, height: 50)
  }
}
extension ViewController: UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return colors.count
  }
   
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let theCell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
    theCell.backgroundColor = self.colors[indexPath.row]
    return theCell
  }
   
  func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    let item = self.colors.remove(at: sourceIndexPath.item)
    self.colors.insert(item, at: destinationIndexPath.item)
  }
}


I have found that with this code, a crash will happen when you try reordering the cells. The crash will occur at self.collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: self.collectionView)) on line 49. I've noticed that setting flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize is what leads to the crash. When I don't set it, it does not crash (on iOS 13 it doesn't crash either way). In tinkering with it further, I've found that if I leave that variable set, but remove the sizeForItemAt delegate method, it will also not crash. While that seems to work as a fine solution here, we do make use of that delegate method in our main app. Does anyone know what is happening here or how to work around this?

I know that there are many other steps to properly implement self-sizing cells and that isn't performed here, but I've found that it doesn't prevent the crash here, as we perform them in our main app. From my understanding, not fully implementing self-sizing cells mostly should mostly lead to UI bugs anyways and not this full on crash, which seems to have popped up in iOS 14.

This is most recently tested with:
iOS 14 Developer Beta 6
Xcode 11.3.1

Replies

Same issue here , same code logic almost as yours , solved the problem by setting collection view Estimated size to „None“ in storyboard, continue use size for item delegate function , does the job for now , but indeed strange , hope it’s just a iOS 14 bug and will be fixed
This is a known issue in iOS 14.0, but it should be fixed in the iOS 14.2 beta published this week.
Thanks for the details.
I was also having a crash, in iOS 14 only, on a Self Sizing Collection View Cells when calling collectionView.moveItem(at: fromIndexPath, to: toIndexPath).
The workaround I've found is to switch estimatedItemSize to CGSize.zero just before and change it back to UICollectionViewFlowLayout.automaticSize after calling moveItem, like this:
Code Block
layout.estimatedItemSize = CGSize.zero
collectionView.moveItem(at: fromIndexPath, to: toIndexPath)
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

This can cause a small glitch in the UI but at least it doesn't crash.
Hope this helps someone.
In my case, it's the choice of UICollectionViewDropProposal. Depending on the drop location, I could either use insertIntoDestinationIndexPath or insertAtDestinationIndexPath. The latter is what causes the app to crash.

Same with the rest of you here that it only occurs on iOS 14. My UICollectionView cells are self-sizing. So I do use automaticSize as my estimatedItemSize value.

Hope they get to fix this soon as that drop insertion is quite intuitive to users. For now, I'll settle with insertInto* while there's no better workaround available.
  • This issue is still a problem after iOS 16+

    In the case of a UICollectionViewDropProposal the workaround is: use the UIDropSession's location(in:).y value (or the view's y value) if session.localDragSession?.location(in: self.view).y > 200 {     return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)    } else {     return UICollectionViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)    }

Add a Comment