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:
- 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)
}
}
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
How to Cache Using Nsurlsession and Nsurlcache. Not Working
Detect App Crashed During Load/Last Time It Was Run
Icloud + Coredata - How to Avoid Pre-Filled Data Duplication
Get List of All Installed Application in iOS 8
Decompilation Possibilities in iOS and How to Prevent Them
Xcode: "Scene Is Unreachable Due to Lack of Entry Points" But Can't Find It
Generics in Swift - "Generic Parameter 'T' Could Not Be Inferred
Make Uitableview Not Scrollable and Adjust Height to Accommodate All Cells
Possible to Limit the Countries an iOS Application Is Released To
Swift 3 How to Get Date for Tomorrow and Yesterday ( Take Care Special Case ) New Month or New Year
Should I Be Using Awakefromnib or Initwithcoder Here
Xcode 8: Preparing Archive Takes Forever
How to Know If an Emoji Is Supported in iOS
Understanding Uilocalnotification Timezone
Any Way to Install App to iPhone 4 with Xcode 8 Beta
Using Arc, Is It Fatal Not to Have an Autorelease Pool for Every Thread