iOS 16.4.1 - UICollectionViewController crashes

Since a recent iOS update somewhere between 16.2 and 16.4.1 a change was introduced that now leads to UICollectionViewController crashing in my app.

The crashes are due to the data source being changed without properly updating the ViewController. However the change is made when the UICollectionView is not displayed which used to be allowed but now causes a crash.

My scenario is this:

UINavigationController

  1. UICollectionViewController
  2. UIImageViewController

My data source is a list of images. When a CollectionView item is tapped the UIImageView will be pushed on top of the navigation stack to display this item. From there it is possible to delete the item which removes it from the data source of the CollectionViewController and leads to the crash. This used to be no problem as the CollectionView is lower on the navigation stack and not displayed at the time the alteration occurs. But this has changed in a recent iOS update.

This issue occurs in iOS 16.4.1 and MacOS 13.3.1 (Apple Silicon).

Here is the stack:

Invalid batch updates detected: the number of sections and/or items returned by the data source before and/or after performing the batch updates are inconsistent with the updates. Data source before updates = { 1 section with item counts: [27386] } Data source after updates = { 1 section with item counts: [27385] } Updates = [ Delete item (0 - 27384), Insert item (0 - 27384) ]

Post not yet marked as solved Up vote post of Tmmn Down vote post of Tmmn
5.6k views

Replies

This particular exception is new as of iOS 16.4. It indicates a bug in either:

  • your implementation of batch updates (e.g. inserts, deletes, and moves you perform on the UICollectionView), or
  • the counts (number of sections, and number of items in each section) returned by your data source.

In this case, the exception message tells you that there is a single section, and one item was deleted from it and then another item was inserted to it, but the data source reported one more item in that section before the updates (27386 items total) than after (27385 items total). Since the updates you performed result in a net change of zero items, the number of items is expected to be the same before & after. So the issue is either that the insert & delete updates performed were wrong, or the data source returned the wrong number of items either before or after the updates.

When you are performing batch updates manually yourself, make sure to always use performBatchUpdates (documentation) and advance your data source to the new state inside the updates closure. Your data source must return the old counts up until the updates closure executes, and must return the new counts after the updates closure finishes executing.

However, the best way to avoid this issue is to adopt UICollectionViewDiffableDataSource, which manages updates of the collection view for you, and eliminates these invalid update issues entirely. If you're new to diffable data source, you can read about it and learn how to use it with this documentation and sample code.

In prior iOS releases, UICollectionView would log an error message to the console for certain cases of invalid updates, and instead fall back to reloadData. Using reloadData is destructive to UI state and can negatively impact performance, as it resets and fully rebuilds the entire collection view including all cells, so it should be avoided when not necessary. However, if you need a short-term workaround for the crash while you investigate a proper fix or adopt diffable data source, you can use reloadData in cases where you aren't able to accurately perform incremental updates.

  • We started getting hit with this crash recently with iOS 16.4 as well. Is there any recommendations on how to accomplish the advancing of the data source to the new state when using a combination of NSFetchedResultsController + UICollectionView? I suppose I could maintain my own count that only gets updated within the performBatchUpdates block but wasn't sure if that was the recommended method.

  • How cloud I catch the error that accurately perform incremental updates failed?

Add a Comment

Was this change from a logged message to a fatal exception documented in the 16.4 Release Notes or elsewhere in UIKit documentation or headers? I can't seem to find it anywhere other than this Developer Fourms post.

In our case, every crash log I've looked at appears to contain the same number of sections and items before and after the updates:

Invalid batch updates detected: the number of sections and/or items returned by the data source before and/or after performing the batch updates are inconsistent with the updates. Data source before updates = { 1 section with item counts: [9] } Data source after updates = { 1 section with item counts: [9] } Updates = [ Insert item (0 - 1), Insert item (0 - 2), Insert item (0 - 3), Insert item (0 - 4), Insert item (0 - 5), Insert item (0 - 6), Insert item (0 - 7), Insert item (0 - 8) ]

another:

Invalid batch updates detected: the number of sections and/or items returned by the data source before and/or after performing the batch updates are inconsistent with the updates. Data source before updates = { 1 section with item counts: [5] } Data source after updates = { 1 section with item counts: [5] } Updates = [ Insert item (0 - 1), Insert item (0 - 2), Insert item (0 - 3), Insert item (0 - 4) ]

Is it expected that this fatal exception occurs even with the before/after counts appear to be the same?

  • Hey, did you manage to solve this issue in your example or did you understand why the exception is thrown in this case? I'm facing the same issue

Add a Comment

I faced "Invalid update: Invalid number of rows" in iOS 16.4 alone when I added newly obtained data from a pagination API call to the data source and performed performBatchUpdates with empty updates closure to add the rows to my CollectionView. The issue persisted even after trying to update my data source within the updates closure of performBatchUpdates. I fixed this issue by manually creating indexpaths for the newly added data in datasource and appending the created indexpath array to the collectionView's index path. Here is the code for the same :-

extension UICollectionView {
    func refreshCollectionView(from: Int, to: Int, completionHandler: (() -> Void)? = nil) {
        if from > to {
            print("'from' index is greater than 'to' index")
            return
        }
        
        self.performBatchUpdates({ [weak self] in
            if from < to {
                var indexpathArray: [IndexPath] = []
                for index in from...to {
                    let indexpath = IndexPath(row: index, section: 0)
                    indexpathArray.append(indexpath)
                }
                self?.insertItems(at: indexpathArray)
            } else if from == to {
                self?.insertItems(at: [IndexPath(row: from, section: 0)])
            }
        }, completion: {_ in
            if let completionHandler = completionHandler {
                completionHandler()
            }
        })
    }
}