SpaceDog SpaceDog - 1 day ago 5
iOS Question

Nightmare with performBatchUpdates crash

I am facing a nightmare of a crash during performBatchUpdates on a collection view.

The problem is basically this: I have a lot of images on a directory on a server. I want to show the thumbnails of those files on a collection view. But the thumbnail have to be downloaded from the server asynchronously. As they arrive they will be inserted on the collection view using something like this:

dispatch_async(dispatch_get_main_queue(),
^{
[self.collectionView performBatchUpdates:^{

if (removedIndexes && [removedIndexes count] > 0) {
[self.collectionView deleteItemsAtIndexPaths:removedIndexes];
}

if (changedIndexes && [changedIndexes count] > 0) {
[self.collectionView reloadItemsAtIndexPaths:changedIndexes];
}

if (insertedIndexes && [insertedIndexes count] > 0) {
[self.collectionView insertItemsAtIndexPaths:insertedIndexes];
}

} completion:nil];
});


the problem is this (I think). Suppose that at time = 0, the collection view has 10 items. I then add 100 more files to the server. The application sees the new files and start downloading the thumbnails. As the thumbnails download they will be inserted on the collection view. But because the downloads can take different times and this download operation is asynchronous, at one point iOS will lost track of how many elements the collection has and the whole thing will crash with this catastrophic infamous message.


*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of items in section 0. The number of items contained in an
existing section after the update (213) must be equal to the number of
items contained in that section before the update (154), plus or minus
the number of items inserted or deleted from that section (40
inserted, 0 deleted) and plus or minus the number of items moved into
or out of that section (0 moved in, 0 moved out).'


The proof I have something fishy is going on is that if I print the count of items on the data set I see exactly 213. So, the dataset matches the correct number and the message is nonsense.

I have had this problem before, here but that was an iOS 7 project. Somehow the problem returned now on iOS 8 and the solutions there are not working and now the dataset IS IN SYNC.

Answer

I think the problem is caused by the indexes.

Key:

  • For updated and deleted items, the indexes have to be the indexes of original items.
  • For inserted items, the indexes have to be the indexes of final items.

Here is a demo code with comments:

class CollectionViewController: UICollectionViewController {

    var items: [String]!

    let before = ["To Be Deleted 1", "To Be Updated 1", "To Be Updated 2", "To Be Deleted 2", "Stay"]
    let after = ["Updated 1", "Updated 2", "Added 1", "Stay", "Added 2"]

    override func viewDidLoad() {
        super.viewDidLoad()

        self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Refresh", style: .Plain, target: self, action: #selector(CollectionViewController.onRefresh(_:)))

        items = before
    }

    func onRefresh(_: AnyObject) {

        items = after

        collectionView?.performBatchUpdates({
            self.collectionView?.deleteItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 3, inSection: 0), ])

            // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete and reload the same index path
            // self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0), NSIndexPath(forRow: 1, inSection: 0), ])

            // NOTE: Have to be the indexes of original list
            self.collectionView?.reloadItemsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0), NSIndexPath(forRow: 2, inSection: 0), ])

            // Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert item 4 into section 0, but there are only 4 items in section 0 after the update'
            // self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 4, inSection: 0), NSIndexPath(forRow: 5, inSection: 0), ])

            // NOTE: Have to be index of final list
            self.collectionView?.insertItemsAtIndexPaths([NSIndexPath(forRow: 2, inSection: 0), NSIndexPath(forRow: 4, inSection: 0), ])

        }, completion: nil)
    }

    override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
    override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath)

        let label = cell.viewWithTag(100) as! UILabel

        label.text = items[indexPath.row]

        return cell
    }
}
Comments