This is an error:
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to delete and reload the same index path ( {length = 2, path = 0 - 0}) with userInfo (null)
This is my typical NSFetchedResultsControllerDelegate:
func controllerWillChangeContent(controller: NSFetchedResultsController) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
let indexSet = NSIndexSet(index: sectionIndex)
switch type {
case .Insert:
tableView.insertSections(indexSet, withRowAnimation: .Fade)
case .Delete:
tableView.deleteSections(indexSet, withRowAnimation: .Fade)
case .Update:
fallthrough
case .Move:
tableView.reloadSections(indexSet, withRowAnimation: .Fade)
}
}
func controller(controller: NSFetchedResultsController, didChangeObject anObject: NSManagedObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
}
case .Update:
if let indexPath = indexPath {
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .None)
}
case .Move:
if let indexPath = indexPath {
if let newIndexPath = newIndexPath {
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Fade)
}
}
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}
in viewDidLoad():
private func setupOnceFetchedResultsController() {
if fetchedResultsController == nil {
let context = NSManagedObjectContext.MR_defaultContext()
let fetchReguest = NSFetchRequest(entityName: "DBOrder")
let dateDescriptor = NSSortDescriptor(key: "date", ascending: false)
fetchReguest.predicate = NSPredicate(format: "user.identifier = %@", DBAppSettings.currentUser!.identifier )
fetchReguest.sortDescriptors = [dateDescriptor]
fetchReguest.fetchLimit = 10
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchReguest, managedObjectContext: context, sectionNameKeyPath: "identifier", cacheName: nil)
fetchedResultsController.delegate = self
try! fetchedResultsController.performFetch()
}
}
This seems to be a bug in iOS 9 (which is still beta) and is also discussed in the Apple Developer Forum
I can confirm the problem with the iOS 9 Simulator from Xcode 7 beta 3.
I observed that for an updated managed object, the didChangeObject: delegate method is called twice: Once with the NSFetchedResultsChangeUpdate event and then again with the NSFetchedResultsChangeMove event (and indexPath == newIndexPath).
Adding an explicit check for indexPath != newIndexPath
as suggested in the above thread seems to solve the problem:
case .Move:
if indexPath != newIndexPath {
tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
}
Update: the described problem occurs only on iOS 8 when building against iOS 9.0 or iOS 9.1 (beta) SDK.
I came up with some horrible workaround today after playing with Xcode 7 beta 6 (iOS 9.0 beta 5) and it seems that it works.
You cannot use reloadRowsAtIndexPaths because in certain cases it's called too early and may cause inconsistency, instead you should manually update your cell.
I still think that the best option is to simply call reloadData.
I believe you can adapt my code for swift with no efforts, I have objective-c project here.
@property NSMutableIndexSet *deletedSections, *insertedSections;
// ...
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
self.deletedSections = [[NSMutableIndexSet alloc] init];
self.insertedSections = [[NSMutableIndexSet alloc] init];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndex:sectionIndex];
switch(type) {
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:indexSet withRowAnimation:UITableViewRowAnimationAutomatic];
[self.deletedSections addIndexes:indexSet];
break;
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:indexSet withRowAnimation:UITableViewRowAnimationAutomatic];
[self.insertedSections addIndexes:indexSet];
break;
default:
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
switch(type) {
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
// iOS 9.0b5 sends the same index path twice instead of delete
if(![indexPath isEqual:newIndexPath]) {
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else if([self.insertedSections containsIndex:indexPath.section]) {
// iOS 9.0b5 bug: Moving first item from section 0 (which becomes section 1 later) to section 0
// Really the only way is to delete and insert the same index path...
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else if([self.deletedSections containsIndex:indexPath.section]) {
// iOS 9.0b5 bug: same index path reported after section was removed
// we can ignore item deletion here because the whole section was removed anyway
[self.tableView insertRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationAutomatic];
}
break;
case NSFetchedResultsChangeUpdate:
// On iOS 9.0b5 NSFetchedResultsController may not even contain such indexPath anymore
// when removing last item from section.
if(![self.deletedSections containsIndex:indexPath.section] && ![self.insertedSections containsIndex:indexPath.section]) {
// iOS 9.0b5 sends update before delete therefore we cannot use reload
// this will never work correctly but at least no crash.
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[self _configureCell:cell forRowAtIndexPath:indexPath];
}
break;
}
}
Xcode 7 / iOS 9.0 only
In Xcode 7 / iOS 9.0 NSFetchedResultsChangeMove is still being sent instead of "update".
As a simple workaround, just disable animations for that case:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableViewRowAnimation animation = UITableViewRowAnimationAutomatic;
switch(type) {
case NSFetchedResultsChangeMove:
// @MARK: iOS 9.0 bug. Move sent instead of update. indexPath = newIndexPath.
if([indexPath isEqual:newIndexPath]) {
animation = UITableViewRowAnimationNone;
}
[self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:animation];
[self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:animation];
break;
// ...
}
}
With regard to this happening on iOS8, with builds compiled against iOS9, on top of the indexPath==newIndexPath problem addressed by some other answers, something else happens which is very weird.
The NSFetchedResultsChangeType enum has four possible values (comments with values are mine):
public enum NSFetchedResultsChangeType : UInt {
case Insert // 1
case Delete // 2
case Move // 3
case Update // 4
}
.. however, the controller:didChangeObject:atIndexPath:forChangeType function is sometimes called with an invalid value 0x0.
Swift seems to default to the first switch case at that point so if you have the following structure:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case .Delete: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case .Update: tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)
case .Move: tableView.moveRowAtIndexPath(ip, toIndexPath: nip)
}
}
.. the invalid call will result in an Insert, and you will get an error like the following:
Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (7) must be equal to the number of rows contained in that section before the update (7), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted)
Simply swapping the cases so that the first case is a rather innocuous Update fixes the problem:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Update: tableView.reloadRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.None)
case .Insert: tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case .Delete: tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
case .Move: tableView.moveRowAtIndexPath(ip, toIndexPath: nip)
}
}
Another option would be checking type.rawValue for an invalid value.
Note: while this addresses a slightly different error message than the one posted by the OP, the issue is related; it's somewhat likely that as soon as you fix the indexPath==newIndexPath problem, this one will pop up.
Also, the above code blocks are simplified to illustrate the sequence; the appropriate guard blocks are missing, for instance - please don't use them as is.
Credits: this was originally discovered by iCN7, source: Apple Developer Forums — iOS 9 CoreData NSFetchedResultsController update causes blank rows in UICollectionView/UITableView
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.
The other answers were close for me, but I was receiving "< invalid > (0x0)" as the NSFetchedResultsChangeType. I noticed that it was being interpreted as an "insert" change. So the following fix worked for me:
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
switch type {
case .Insert:
// iOS 9 / Swift 2.0 BUG with running 8.4
if indexPath == nil {
self.tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: UITableViewRowAnimation.Fade)
}
(etc...)
}
Since every "insert" only comes back with a newIndexPath and no indexPath (and this strange extra insert delegate call is coming back with the same path listed for both newIndexPath and indexPath), this just checks that it's the right kind of "insert" and skips the others.
The problem happened because of reload and delete the same indexPath(which a bug produced by apple),so I change the way I handle the NSFetchedResultsChangeUpdate message.
Instead of:
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
I updated the content of the cell manually:
MyChatCell *cell = (MyChatCell *)[self.tableView cellForRowAtIndexPath:indexPath];
CoreDataObject *cdo = [[self fetchedResultsController] objectAtIndexPath:indexPath];
// update the cell with the content: cdo
[cell updateContent:cdo];
It turns out to be working well.
BTW:
the update of CoreData object would produce a delete and a insert message.To update the cell content correctly,when the indexPath is equal to the newIndexPath(both the section and row are equal),I reload the cell by[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Here is the sample code:
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
if (![self isViewLoaded]) return;
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:{
MyChatCell *cell = (MyChatCell *)[self.tableView cellForRowAtIndexPath:indexPath];
CoreDataObject *cdo = [[self fetchedResultsController] objectAtIndexPath:indexPath];
// update the cell with the content: cdo
[cell updateContent:cdo];
}
break;
case NSFetchedResultsChangeMove:
if (indexPath.row!=newIndexPath.row || indexPath.section!=newIndexPath.section){
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
}else{
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
}
}
I putted the sample code above to gist: https://gist.github.com/dreamolight/157266c615d4a226e772
来源:https://stackoverflow.com/questions/31383760/ios-9-attempt-to-delete-and-reload-the-same-index-path