iOS 9 - "Attempt to Delete and Reload the Same Index Path"

iOS 9 - attempt to delete and reload the same index path

For some reason NSFetchedResultsController calls .Update followed by .Move after controllerWillChangeContent: is called.

Simply it looks like this: BEGIN UPDATES -> UPDATE -> MOVE -> END UPDATES.

Happens only under iOS 8.x

During one session of update the same cell is reloaded and deleted what cause a crash.

THE SIMPLEST FIX EVER:

The following part of code:

case .Update:
if let indexPath = indexPath {
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}

replace with:

case .Update:
if let indexPath = indexPath {

// 1. get your cell
// 2. get object related to your cell from fetched results controller
// 3. update your cell using that object

//EXAMPLE:
if let cell = tableView.cellForRowAtIndexPath(indexPath) as? WLTableViewCell { //1
let wishlist = fetchedResultsController.objectAtIndexPath(indexPath) as! WLWishlist //2
cell.configureCellWithWishlist(wishlist) //3
}
}

THAT REALLY WORKS.

CRASH attempt to delete and reload the same index path

I was able to reproduce this today. To do this you need to:

  1. Open your app that is listening for changes
  2. Open the photos app, save a set of photos to your photo library from an iCloud shared album
  3. Go to the photos app, delete some of those photos
  4. 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)
}
}

How to order moves, inserts, deletes, and updates in a UICollectionView performBatchUpdates block?

For the move operations, the from indexPath is pre-delete indices, and the to indexPath is post-delete indices. Reloads should only be specified for indexPaths that have not been inserted, deleted, or moved. This is probably why you're seeing the NSInternalInconsistencyException.

A handy way to verify the operations are set up correctly: the set of reload, insert and move-to indexPaths should not have any duplicates, and the set of reload, delete, and move-from indexPaths should not have any duplicates.

UPDATE:

It appears that items that you move are not also updated, but only moved. So, if you need to update and move an item, you can perform the reload before or after the batch update (depending on the state of your data source).

Terminating app due to uncaught exception, reason: 'attempt to delete row 3 from section 1 which only contains 0 rows before the update'

I just fix the same exception. It's mainly because you call reloadRows(at: with:) with indexPath(s) whose row is larger than the total row count.

Animation issue while bouncing UIVCollectionView and reload section at the same time

Well, since no one suggested a solution, I can offer two solutions that I've researched, with their pros and cons

Before...

The problem is detected only when collectionView.contentOffset.y is negative and section inserts.

Approach 1

While we scrolling / holding any collection our main thread RunLoop has tracking mode and if iOS receive notification about section inserting, this operation is performed in common mode. It updates immediately, possibly neglecting smoothness. The solution is to use

 RunLoop.current.perform(inModes: [.default]) { /* Perform updates */ } 

The main minus is an escaping block and consequently asynchronous updates. Not so good.

Approach 2

Since I can't use async updates in my case I just made that

 func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let mode = RunLoop.current.currentMode, mode == .tracking, scrollView.contentOffset.y == 0 else {
previousOffset = scrollView.contentOffset
return
}
scrollView.setContentOffset(previousOffset, animated: false)
}

Don't think its brilliant solution but I have no idea how to fix it another way.

UICollectionView BatchUpdate edge case fails

It does seem to be a bug, although while the documentation for performBatchUpdates:completion explains the context for indices in insert and delete operations, and mentions that reload operations are permitted, it doesn't detail the context for indices in reload operations.

After some experimentation, it seems that "under the covers" a reload is implemented as a delete and an insert. This seems to cause an issue where there is an overlap between some other operation and the reload index.

I did find, however, that replacing the reload with an explicit delete and insert seems to work, so you can use:

- (void)triggerBatchUpdate {
[_collection performBatchUpdates:^{
_values = @[[UIColor blueColor], [UIColor yellowColor], [UIColor redColor]];
[_collection moveItemAtIndexPath:[self index:0] toIndexPath:[self index:2]];
[_collection insertItemsAtIndexPaths:@[[self index:0]]];

[_collection deleteItemsAtIndexPaths:@[[self index:1]]];
[_collection insertItemsAtIndexPaths:@[[self index:1]]];
} completion:nil];
}


Related Topics



Leave a reply



Submit