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]];
}
}
}