CRASH attempt to delete and reload the same index path
I was able to reproduce this today. To do this you need to:
- Open your app that is listening for changes
- Open the photos app, save a set of photos to your photo library from an iCloud shared album
- Go to the photos app, delete some of those photos
- Go again to the iCloud shared album and save again the some of the photos you deleted. You'll see this condition happen.
I found an updated code that seems to work better to handle the updating behavior here:https://developer.apple.com/library/ios/documentation/Photos/Reference/PHPhotoLibraryChangeObserver_Protocol/
But it still doesn't handle this situation nor when the indexes to be deleted are bigger (i.e. Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 9 from section 0 which only contains 9 items before the update'). I created this updated version of this code that deals with this better and hasn't crashed for me anymore so far.
func photoLibraryDidChange(changeInfo: PHChange!) { // Photos may call this method on a background queue; // switch to the main queue to update the UI. dispatch_async(dispatch_get_main_queue()) { // Check for changes to the list of assets (insertions, deletions, moves, or updates). if let collectionChanges = changeInfo.changeDetailsForFetchResult(self.assetsFetchResult) { // Get the new fetch result for future change tracking. self.assetsFetchResult = collectionChanges.fetchResultAfterChanges if collectionChanges.hasIncrementalChanges { // Get the changes as lists of index paths for updating the UI. var removedPaths: [NSIndexPath]? var insertedPaths: [NSIndexPath]? var changedPaths: [NSIndexPath]? if let removed = collectionChanges.removedIndexes { removedPaths = self.indexPathsFromIndexSetWithSection(removed,section: 0) } if let inserted = collectionChanges.insertedIndexes { insertedPaths = self.indexPathsFromIndexSetWithSection(inserted,section: 0) } if let changed = collectionChanges.changedIndexes { changedPaths = self.indexPathsFromIndexSetWithSection(changed,section: 0) } var shouldReload = false if changedPaths != nil && removedPaths != nil{ for changedPath in changedPaths!{ if contains(removedPaths!,changedPath){ shouldReload = true break } } } if removedPaths?.last?.item >= self.assetsFetchResult.count{ shouldReload = true } if shouldReload{ self.collectionView.reloadData() }else{ // Tell the collection view to animate insertions/deletions/moves // and to refresh any cells that have changed content. self.collectionView.performBatchUpdates( { if let theRemovedPaths = removedPaths { self.collectionView.deleteItemsAtIndexPaths(theRemovedPaths) } if let theInsertedPaths = insertedPaths { self.collectionView.insertItemsAtIndexPaths(theInsertedPaths) } if let theChangedPaths = changedPaths{ self.collectionView.reloadItemsAtIndexPaths(theChangedPaths) } if (collectionChanges.hasMoves) { collectionChanges.enumerateMovesWithBlock() { fromIndex, toIndex in let fromIndexPath = NSIndexPath(forItem: fromIndex, inSection: 0) let toIndexPath = NSIndexPath(forItem: toIndex, inSection: 0) self.collectionView.moveItemAtIndexPath(fromIndexPath, toIndexPath: toIndexPath) } } }, completion: nil) } } else { // Detailed change information is not available; // repopulate the UI from the current fetch result. self.collectionView.reloadData() } } }}func indexPathsFromIndexSetWithSection(indexSet:NSIndexSet?,section:Int) -> [NSIndexPath]?{ if indexSet == nil{ return nil } var indexPaths:[NSIndexPath] = [] indexSet?.enumerateIndexesUsingBlock { (index, Bool) -> Void in indexPaths.append(NSIndexPath(forItem: index, inSection: section)) } return indexPaths}
Swift 3 / iOS 10 version:
func photoLibraryDidChange(_ changeInstance: PHChange) { guard let collectionView = self.collectionView else { return } // Photos may call this method on a background queue; // switch to the main queue to update the UI. DispatchQueue.main.async { guard let fetchResults = self.fetchResults else { collectionView.reloadData() return } // Check for changes to the list of assets (insertions, deletions, moves, or updates). if let collectionChanges = changeInstance.changeDetails(for: fetchResults) { // Get the new fetch result for future change tracking. self.fetchResults = collectionChanges.fetchResultAfterChanges if collectionChanges.hasIncrementalChanges { // Get the changes as lists of index paths for updating the UI. var removedPaths: [IndexPath]? var insertedPaths: [IndexPath]? var changedPaths: [IndexPath]? if let removed = collectionChanges.removedIndexes { removedPaths = self.indexPaths(from: removed, section: 0) } if let inserted = collectionChanges.insertedIndexes { insertedPaths = self.indexPaths(from:inserted, section: 0) } if let changed = collectionChanges.changedIndexes { changedPaths = self.indexPaths(from: changed, section: 0) } var shouldReload = false if let removedPaths = removedPaths, let changedPaths = changedPaths { for changedPath in changedPaths { if removedPaths.contains(changedPath) { shouldReload = true break } } } if let item = removedPaths?.last?.item { if item >= fetchResults.count { shouldReload = true } } if shouldReload { collectionView.reloadData() } else { // Tell the collection view to animate insertions/deletions/moves // and to refresh any cells that have changed content. collectionView.performBatchUpdates({ if let theRemovedPaths = removedPaths { collectionView.deleteItems(at: theRemovedPaths) } if let theInsertedPaths = insertedPaths { collectionView.insertItems(at: theInsertedPaths) } if let theChangedPaths = changedPaths { collectionView.reloadItems(at: theChangedPaths) } collectionChanges.enumerateMoves { fromIndex, toIndex in collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0), to: IndexPath(item: toIndex, section: 0)) } }) } } else { // Detailed change information is not available; // repopulate the UI from the current fetch result. collectionView.reloadData() } } }}func indexPaths(from indexSet: IndexSet?, section: Int) -> [IndexPath]? { guard let set = indexSet else { return nil } return set.map { (index) -> IndexPath in return IndexPath(item: index, section: section) }}
I just moved the reloadItemsAtIndexPaths
after the batch updates are completed to fix the crash of deleting and reloading at the same time.
From docs of changedIndexes
of PHFetchResultChangeDetails
:
These indexes are relative to the original fetch result (the fetchResultBeforeChanges property) after you’ve applied the changes described by the removedIndexes and insertedIndexes properties; when updating your app’s interface, apply changes after removals and insertions and before moves.
PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults];[collectionView performBatchUpdates:^{ NSIndexSet *removedIndexes = [collectionChanges removedIndexes]; if ([removedIndexes count]) { [collectionView deleteItemsAtIndexPaths:[self indexPathsFromIndexes:removedIndexes withSection:0]]; } NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes]; if ([insertedIndexes count]) { [collectionView insertItemsAtIndexPaths:[self indexPathsFromIndexes:insertedIndexes withSection:0]]; } } completion:^(BOOL finished) { if (finished) { // Puting this after removes and inserts indexes fixes a crash of deleting and reloading at the same time. // From docs: When updating your app’s interface, apply changes after removals and insertions and before moves. NSIndexSet *changedIndexes = [collectionChanges changedIndexes]; if ([changedIndexes count]) { [collectionView reloadItemsAtIndexPaths:[self indexPathsFromIndexes:changedIndexes withSection:0]]; } } }
I implemented the code in batkryu's answer in Objective-C.
- (void)photoLibraryDidChange:(PHChange *)changeInstance { dispatch_async(dispatch_get_main_queue(), ^{ PHFetchResultChangeDetails *collectionChanges = [changeInstance changeDetailsForFetchResult:self.assetsFetchResults]; if (collectionChanges) { self.assetsFetchResults = [collectionChanges fetchResultAfterChanges]; UICollectionView *collectionView = self.collectionView; NSArray *removedPaths; NSArray *insertedPaths; NSArray *changedPaths; if ([collectionChanges hasIncrementalChanges]) { NSIndexSet *removedIndexes = [collectionChanges removedIndexes]; removedPaths = [self indexPathsFromIndexSet:removedIndexes withSection:0]; NSIndexSet *insertedIndexes = [collectionChanges insertedIndexes]; insertedPaths = [self indexPathsFromIndexSet:insertedIndexes withSection:0]; NSIndexSet *changedIndexes = [collectionChanges changedIndexes]; changedPaths = [self indexPathsFromIndexSet:changedIndexes withSection:0]; BOOL shouldReload = NO; if (changedPaths != nil && removedPaths != nil) { for (NSIndexPath *changedPath in changedPaths) { if ([removedPaths containsObject:changedPath]) { shouldReload = YES; break; } } } if (removedPaths.lastObject && ((NSIndexPath *)removedPaths.lastObject).item >= self.assetsFetchResults.count) { shouldReload = YES; } if (shouldReload) { [collectionView reloadData]; } else { [collectionView performBatchUpdates:^{ if (removedPaths) { [collectionView deleteItemsAtIndexPaths:removedPaths]; } if (insertedPaths) { [collectionView insertItemsAtIndexPaths:insertedPaths]; } if (changedPaths) { [collectionView reloadItemsAtIndexPaths:changedPaths]; } if ([collectionChanges hasMoves]) { [collectionChanges enumerateMovesWithBlock:^(NSUInteger fromIndex, NSUInteger toIndex) { NSIndexPath *fromIndexPath = [NSIndexPath indexPathForItem:fromIndex inSection:0]; NSIndexPath *toIndexPath = [NSIndexPath indexPathForItem:toIndex inSection:0]; [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; }]; } } completion:NULL]; } [self resetCachedAssets]; } else { [collectionView reloadData]; } } });}- (NSArray *)indexPathsFromIndexSet:(NSIndexSet *)indexSet withSection:(int)section { if (indexSet == nil) { return nil; } NSMutableArray *indexPaths = [[NSMutableArray alloc] init]; [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { [indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]]; }]; return indexPaths;}