PerformBatchUpdates failed nightmare

I encountered the nightmarish performBatchUpdates during performBatchUpdates in a collection view .

The problem is mainly this: I have a lot of images in a directory on the server. I want to show thumbnails of these files in collection view . But the thumbnail must be loaded from the server asynchronously. Upon arrival, they will be inserted into 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). Assume that at time 0, the collection view contains 10 elements. Then I add another 100 files to the server. The application will see new files and start downloading thumbnails. As the thumbnails load, they will be inserted into the collection view. But since loading may take different times, and this loading operation is asynchronous , at some point iOS will lose track of the number of items in the collection, and all this will fail with this catastrophic, infamous message.

*** The application terminated due to an unhandled exception "NSInternalInconsistencyException", reason: "Invalid update: invalid number of elements in section 0. The number of elements contained in an existing section after the update (213) must be equal to the number of elements contained in this section before the update (154), plus or minus the number of elements inserted or deleted from this section (40 inserted, 0 deleted) and plus or minus the number of elements moved to or from this section (0 moved to 0 moved).

The proof that I have something suspicious is that if I print out the number of elements in the data set, I see exactly 213. Thus, the data set corresponds to the correct number, and the message is meaningless.

I had this problem before, here, but it was an iOS 7 project. Somehow the problem returned now on iOS 8, and the solutions do not work there, and now the data set is in sync mode.

+9
source share
4 answers

I think the problem is caused by indexes.

Key:

  • For updated and deleted items, the indices must be the indices of the source items.
  • For inserted elements, indexes must be finite element indexes.

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 } } 
+3
source

It looks like you need to do more work with batch processing, which images appeared for each animated group . performBatchUpdates of how you performBatchUpdates like before, the performBatchUpdates method performBatchUpdates :

  1. Before calling your block, it double-checks the total number of elements and saves them by calling numberOfItemsInSection (this is 154 in your error message).
  2. It starts the block, tracking insertions / deletions and calculates how many final elements should be based on insertions and deletions.
  3. After starting the block, it double-checks the values ​​calculated by it to the actual values ​​when it asks for your data source numberOfItemsInSection (this is the number 213). If it does not match, it will crash.

Based on your insertedIndexes changedIndexes and changedIndexes you first calculate what things should be displayed based on the response to the download from the server, and then run the package. However, I assume that your numberOfItemsInSection method always just returns the "true" number of elements.

Thus, if the download completes in step 2, when it performs a health check at '3', your numbers will no longer line up.

batchUpdates solution: wait until all files are downloaded, and then run one batchUpdates . Probably not the best user experience, but this avoids this problem.

A more complex solution: Perform batches as needed and keep track of which items have already been detected / are being animated separately from the total number of items. Sort of:

 BOOL _performingAnimation; NSInteger _finalItemCount; - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return _finalItemCount; } - (void)somethingDidFinishDownloading { if (_performingAnimation) { return; } // Calculate changes. dispatch_async(dispatch_get_main_queue(), ^{ _performingAnimation = YES; [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]; } _finalItemCount += (insertedIndexes.count - removedIndexes.count); } completion:^{ _performingAnimation = NO; }]; }); } 

The only thing that can be solved after this is to make sure that you run one last check for the remaining items, if the last item to load completed during the animation (maybe there is a performFinalAnimationIfNeeded method that you run in the completion block)

+4
source

For those who have a similar problem, let me quote the documentation on the UICollectionView :

If the collection view layout is not updated before calling this method, a reboot may occur. To avoid problems, you must update the data model in the update block or make sure the layout is updated before calling performBatchUpdates(_:completion:) .

Initially, I referenced an array of a separate model object, but decided to save a local copy of the array in the view controller and update the array in performBatchUpdates(_:completion:) .

The problem was solved.

+2
source

This can happen because you also need to make sure that with collectionViews you can delete and insert sections. when you try to insert an item into a section that does not exist, you will get this failure.

Preform batch updates do not know that you wanted to add an X + 1 section when you insert an element in X + 1, X. without adding this section to.

0
source

All Articles