Nightmare with performBatchUpdates crash
It sounds like you need to do a bit extra work with batching which images have appeared for each animation group
. From dealing with crashes like this before, the way performBatchUpdates
works is
- Before invoking your block, it double checks all the item counts and saves them by calling
numberOfItemsInSection
(this is the 154 in your error message). - It runs the block, tracking the inserts/deletes, and calculates what the final number of items should be based on the insertions and deletions.
- After the block is run, it double checks the counts it calculated to the actual counts when it asks your dataSource
numberOfItemsInSection
(this is the 213 number). If it doesn't match, it will crash.
Based on your variables insertedIndexes
and changedIndexes
, you're pre-calculating which things need to show up based on the download response from server, and then running the batch. However I'm guessing your numberOfItemsInSection
method is always just returning the 'true' count of items.
So if a download completes during step 2, when it performs the sanity check in '3', your numbers won't line up anymore.
Easiest solution: Wait until all files have downloaded, then do a single batchUpdates
. Probably not the best user experience but it avoids this issue.
Harder solution: Perform batches as needed, and track which items have already shown up / are currently animating separately from the total number of items. Something like:
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 to solve after that would be to make sure you run one final check for leftover items if the last item to download finished during an animation (maybe have a method performFinalAnimationIfNeeded
that you run in the completion block)
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
}
}