Can't delete last object from fetchedResultsController - ios

i use NSFetchedResultsController to populate tableview and every thing is ok(adding, deletng..).. But whenever i try to remove last row from tableview(when there is only one row in tablview) the app crashes with the log below:
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1914.84/UITableView.m:1037
2012-08-03 16:32:39.667 MackaWithCoreData[793:15503] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
As far as i see, after deleting the object when the delegate methods try to reload the tableview since the fetchedResultsController is null it causes a crash.. How to handle this issue? Thanks in advance
EDIT Implementation of controller controllerDidChangeContent:... method is below
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
XLog(#"the row removed from tv");
break;
case NSFetchedResultsChangeUpdate:
// [self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
XLog(#" object deleted from fetchedresultscontroller");
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}

After a few days i decided a different logic to solve this issue. I firstly control the count of fetchedObjects on fetchedResultsController, if the count is greater than 1 i call deletion & saving delegates on database, but if the count is equal to 1, just push to a different viewController. On that vc too, i'll disable navigation controller back to this view controller untill a new message comes. And when a new message comes, i will firstly delete the object on entity and than add the message..
Here is some pseudo code & algorithm:
if ([[self.fetchedResultsController fetchedObjects]count] >1) {
callDeletionDelegateForDatabase;
}else{
declare a viewController object;
pushToThatVC;
}
// on the class of target vc
hide_back_navigation_back_to_theViewControllerYouCameFrom;
//when a new message comes from web service
reset Entitiy(the property we need its value)
show_navigation_Controller_To_theViewControllerOfMessages...
I posted this stupidly coded pseudo to help others who faces the same situation, if anyone knows a better approach, please write down.
Below is my code:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
if ([[self.fetchedResultsController fetchedObjects]count] >1) {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}else {
BNT_DetailViewController *vc=[BNT_DetailViewController new];
[self.navigationController pushViewController:vc animated:YES];
XLog(#"You Cant delete this");
}
}
}

Related

UITableView and NSFetchedResultsController row deletion error [duplicate]

Deleting a row from a UITableView fed by an NSFetchedResultsController causes my app to crash.
Error is:
* Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-2903.23/UITableView.m:1330
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
I want only swipe-to-delete. My deletion code goes like this:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView beginUpdates];
SequenceData *sd = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.managedObjectContext deleteObject:sd];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
}
My NSFetchedResultsController is set up just like in Ray Wanderlich's tutorial (http://www.raywenderlich.com/999/core-data-tutorial-for-ios-how-to-use-nsfetchedresultscontroller)
Number of rows is determined by:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id sectionObjects = [[self.fetchedResultsController sections] objectAtIndex:section];
NSInteger nbObjects = [sectionObjects numberOfObjects];
return nbObjects;
}
It looks like the fetch is not updating (number of rows does not vary). Do I need to fetch myself (isn't this taking care of by the fetch controller)?
There is obviously something basic I am missing here. Don't hesitate to suggest basic answers.
I first implemented this with the controller: didChangeObject: ... method. Error was the same, but some details differed.
Edit
I believe my problem is fixed. Both answers (From CX and Martin) helped me find it. Martin got the answer because of the explanations that helped me understand a little bit better...
Don't call
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
if you use a fetched results controller. Only delete the object with
[self.managedObjectContext deleteObject:sd];
The fetched results controller delegate method didChangeObject: is then called automatically,
and that calls deleteRowsAtIndexPaths:.
So in your case, the row was deleted twice, and that caused the exception.
Note that you don't need beginUpdates/endUpdates here.
If you want to delete a row you need to delete managedObject only.
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[self.managedObjectContext deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
// handle error
}
}
Deleting the managed object triggers the NSFetchResultController delegate methods, and they will update the tableView.
Edit
You should implement NSFetchResultController delegate method
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath{
switch(type) {
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
////
default:
break;
}
Because when you work with data source like NSFetchedResultsController, all changes must come from there and your table only reflects them.

How to properly handle Collapsable Sections updates in an UITableView (Serious application update during a call to -controllerDidChangeContent)

I am working upon a Core-data app which involves collapsable sections.
Basically, if I move a managed object across section, or just delete it when its section is opened, I get the following error:
2014-04-28 19:38:44.690 uRSS[663:60b] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 7. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
I believe its about the way I handle changes in the didChange Object method but I am stuck ATM:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"MasterViewController::didChangeObject **** Inserting something in %#**** ", _detailViewController.detailItem.category.name);
if ([_detailViewController.detailItem.category.open boolValue]) {
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeDelete:
NSLog(#"MasterViewController::didChangeObject **** Deleting something in %#**** ", _detailViewController.detailCategory.name);
if ([_detailViewController.detailCategory.open boolValue]) {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeUpdate:
NSLog(#"MasterViewController::didChangeObject **** Changing something in %#**** ", _detailViewController.detailCategory.name);
if ([_detailViewController.detailCategory.open boolValue]) {
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
break;
case NSFetchedResultsChangeMove:
NSLog(#"MasterViewController::didChangeObject **** Moving something from %# to %#**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);
if ([_detailViewController.detailCategory.open boolValue]) {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
if ([_detailViewController.detailItem.category.open boolValue]) {
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
}
My detailView has 2 objects attached to it: detailItem, a feed, and detailCategory, the original Category, should it change.
Category has a name and an openproperty. If open is false, the Category (section) is collapsed.
So far I cannot delete objects or change their category without a crash.
If the category is closed, I can delete them.
Whenever creating a new object (which has its own "new" category), it wrongly appears in the currently opened category. If no category is opened, then it will expectedly crete the new category with the new object.
Can somebody tell me how to handle this, please?
UPDATE(1): Here's some more from the UIDataSource:
This is when and how I move a Feed across Categories in my detailView:
-(void) updateCategory:(id)sender
{
if (!_categoriesPopover) {
return;
}
[_categoriesPopover dismissPopoverAnimated:YES];
[_mainMOC performBlockAndWait:^{
Category *category=_categoryView.categoryChosen;
if (!category) {
return;
}
self.detailItem.category.open=[NSNumber numberWithBool:NO];
self.detailCategory = self.detailItem.category;
self.detailItem.category=category;
self.detailItem.category.open=[NSNumber numberWithBool:YES];
NSError *error;
if (![_mainMOC save:&error])
{
NSLog(#"DetailViewController::updateCategory Error saving context: %#", error);
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:#"feedUpdatedInDetailView" object:nil];
}
}];
}
And it happens once I have clicked on the chosen destination Category in this screenshot:
UPDATE(2):
Here's the numberOfRowsInSection method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (tableView == self.searchDisplayController.searchResultsTableView) {
return [_filteredCategoryArray count];
} else {
if ([[[self.fetchedResultsController sections][section] objects] count]>0) {
Category *cat = ((Feed *)[[[self.fetchedResultsController sections][section] objects] objectAtIndex:0]).category;
return [cat.open boolValue] ? [[self.fetchedResultsController sections][section] numberOfObjects] : 0;
} else {
return 0;
}
}
}
UPDATE(3):
I NSLogged some variables in order to see what was happening and I think we can forget the open/close aspect here.
a) Here's what happens in the DetailView:
-(void) updateCategory:(id)sender
{
if (!_categoriesPopover) {
return;
}
[_categoriesPopover dismissPopoverAnimated:YES];
Category *category=_categoryView.categoryChosen;
if (!category) {
return;
}
[_mainMOC performBlockAndWait:^{
NSLog(#"BEFORE: oldcat: %# (%d) / newcat: %# (%d)", _detailItem.category.name, [_detailItem.category.feeds count], category.name, [category.feeds count]);
self.detailItem.category.open=[NSNumber numberWithBool:NO];
self.detailCategory = self.detailItem.category;
self.detailItem.category=category;
self.detailItem.category.open=[NSNumber numberWithBool:YES];
NSError *error;
if (![_mainMOC save:&error])
{
NSLog(#"DetailViewController::updateCategory Error saving context: %#", error);
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:#"feedUpdatedInDetailView" object:nil];
}
NSLog(#"AFTER: oldcat: %# (%d) / newcat: %# (%d)", _detailCategory.name, [_detailCategory.feeds count], _detailItem.category.name, [_detailItem.category.feeds count]);
}];
}
b) Here's what's in the MasterView:
case NSFetchedResultsChangeMove:
NSLog(#"MVC: oldcat: %# (%d) / newcat: %# (%d)", _detailViewController.detailCategory.name, [_detailViewController.detailCategory.feeds count], _detailViewController.detailItem.category.name, [_detailViewController.detailItem.category.feeds count]);
NSLog(#"MasterViewController::didChangeObject **** Moving something from %# to %#**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
c) Here's what the log says:
2014-04-30 18:52:04.771 uRSS[465:60b] BEFORE: oldcat: trucs (2) / newcat: Misc (7)
2014-04-30 18:52:04.771 uRSS[465:60b] MVC: oldcat: trucs (1) / newcat: Misc (8)
2014-04-30 18:52:04.772 uRSS[465:60b] MasterViewController::didChangeObject **** Moving something from trucs to Misc****
2014-04-30 18:52:04.772 uRSS[465:60b] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2935.137/UITableView.m:1368
2014-04-30 18:52:04.772 uRSS[465:60b] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 5. The number of rows contained in an existing section after the update (8) must be equal to the number of rows contained in that section before the update (0), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
2014-04-30 18:52:04.773 uRSS[465:60b] [AppDelegate::saveContextChanges] merged.
2014-04-30 18:52:04.774 uRSS[465:8c03] [AppDelegate::saveContextChanges] merged.
2014-04-30 18:52:04.775 uRSS[465:60b] AFTER: oldcat: trucs (1) / newcat: Misc (8)
What I don't get is that the DVC works in a PerformBlockAndWait loop but the MVC gets called with the new values before they seem to be committed on the DVC. How is that possible?
What's wrong???
UPDATE(4):
I did as Marcus suggested and removed the MOC save.
The logs look better (BEFORE and AFTER happen in the DVC, MVC happens expectedly after in the MVC and the values are correct):
2014-05-01 07:27:39.391 uRSS[742:60b] BEFORE: oldcat: Misc (8) / newcat: trucs (1)
2014-05-01 07:27:39.392 uRSS[742:60b] AFTER: oldcat: Misc (7) / newcat: trucs (2)
2014-05-01 07:27:39.400 uRSS[742:60b] MVC: oldcat: Misc (7) / newcat: trucs (2)
But:
I still get the error:
2014-05-01 07:27:39.400 uRSS[742:60b] MasterViewController::didChangeObject **** Moving something from Misc to trucs****
2014-05-01 07:27:39.401 uRSS[742:60b] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-2935.137/UITableView.m:1368
2014-05-01 07:27:39.401 uRSS[742:60b] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 5. The number of rows contained in an existing section after the update (0) must be equal to the number of rows contained in that section before the update (8), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
I also see that on restarting the app, the Feed has not been moved which usually happened with a save...
UPDATE(5): I reverted to my original didChangeObject method and it works (moving Feeds across Categories, that is).
How can I get a save to "block" the main UI during its processing? performBlockAndWaitobviously doesn't suffice.
You have a mismatch between what the delegate methods of NSFetchedResultsController are telling the UITableView and what the methods of UITableViewDataSource are telling the table view.
I would suggest looking at your UITableViewDataSource methods, use the debugger and make sure that they are reporting back the same number of rows as your NSFetchedResultsControllerDelegate methods.
That is the source of this error.
Update
There is a disconnect between your -controller: didChangeObject: atIndexPath: forChangeType: newIndexPath: and one of the methods in UITableViewDataSource. From the error I am guessing it is in -tableView: numberOfRowsInSection: which is the method I wish you would add to your question.
When the NSFetchedResultsControllerDelegate finishes it will then ping the methods implemented from the UITableViewDataSource protocol and ask what the table looks like. Those answers MUST match. If they do not, you get this error.
If you run this in the debugger and put breakpoints in these methods and follow along on a piece of paper you can usually track down where the disconnect is.
Update
Looks like you are storing category open state in Core Data and then responding to the number of rows based on whether the category is open or not. Hopefully I am reading that right.
If that is the case then I would suggest turning that logic off and see if the crash goes away. That will narrow your focus to that logic. It is quite possible that your state is getting out of sync.
While not a direct answer to your question, you may find this answer useful as alternative to building your own collapsable table view.: TLIndexPathTools integration with core data for expandable tableView.
As discussed in the link, TLIndexPathTools provides a collapsible table view implementation that integrates with Core Data out of the box.
Quick check- you are implementing the delegate methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
And:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
Yes?
In your NSFetchedResultControllerDelegate method I think you are getting tripped up by referring repeatedly to your properties in your View Controllers for the category and for the open status of that category. What I believe you should do is stick with the changed object.
Your detail item category is out of sync with the object. Start by changing the delegate method to this:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
Feed *changedItem = (Feed *)object
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"MasterViewController::didChangeObject **** Inserting something in %#**** ", changedItem.category.name);
if ([changedItem.category.open boolValue]) {
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeDelete:
NSLog(#"MasterViewController::didChangeObject **** Deleting something in %#**** ", changedItem.category.name);
if ([_detailViewController.detailCategory.open boolValue]) {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeUpdate:
NSLog(#"MasterViewController::didChangeObject **** Changing something in %#**** ", changedItem.category.name);
if ([_detailViewController.detailCategory.open boolValue]) {
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
break;
case NSFetchedResultsChangeMove:
NSLog(#"MasterViewController::didChangeObject **** Moving something from %# to %#**** ", _detailViewController.detailCategory.name, _detailViewController.detailItem.category.name);
// I'm certain that here is where you are out of sync. If changedItem.category
// is open, then all you need do is insert the moved item. If it is closed
// you would need to remove all the rows in the category it came from.
//
// The problem arises because category.open state does not necessarily reflect
// the current state of the section. If the table or section is not yet refreshed
// then the table state is unknown. We need to know the state of the table at the time
// of the move in order to know what to do with the rows. On endUpdates the table will
// call its delegate methods and they must match the results of what we have done here.
// As a quick fix, given that I do not have all your models or project to work with,
// We could set all categories open state to closed and collapse all sections prior to making
// changes here. Better model would be to make the table respond directly any time a
// category objects open state is changed. You could do that with KVO or NSNotifications perhaps.
// In that scenario, you could always count on the table and the model to be in sync.
if ([changedItem.category.open boolValue]) {
// This becomes way complicated if you have not sync'd open and closed sections to match the category states prior to saving your context.
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
} else {
// Again, if your table sections collapse state is in sync (and only one section
// is ever open at one time) you need do nothing. Otherwise you'll need some gyrations
// here to get everything to match what the delegate methods are ultimately going to return
}
break;
}
}
The bug has been gone after I edited the didChangeObject method the following way (I have to take the open flag into account):
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.tableView;
Category *category = ((Feed *) anObject).category;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"MasterViewController::didChangeObject **** Inserting something in %#**** ", category.name);
if ([category.open boolValue]) {
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeDelete:
NSLog(#"MasterViewController::didChangeObject **** Deleting something in %#**** ", _detailViewController.detailCategory.name);
if ([_detailViewController.detailCategory.open boolValue]) {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeUpdate:
NSLog(#"MasterViewController::didChangeObject **** Changing something in %#**** ", category.name);
if ([category.open boolValue]) {
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
break;
case NSFetchedResultsChangeMove:
NSLog(#"MasterViewController::didChangeObject **** Moving something from %# to %#**** ", _detailViewController.detailCategory.name, category.name);
if ([_detailViewController.detailCategory.open boolValue]) {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
if ([category.open boolValue]) {
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
}

ios 7 unstable animation issue when deleting a row in a table view

I have a table view with a search bar/display controller (all built in story board).
My problem is with deleting rows. The functionality of delete row works BUT the animation of row deletion seems to be unstable. Sometimes the deletion is animated and most of the times the row is removed immediately with no animation.
This is my delete method:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
if (tableView == self.searchDisplayController.searchResultsTableView) {
[_managedObjectContext deleteObject:[_notesFilteredFetcher objectAtIndexPath:indexPath]];
} else {
[_managedObjectContext deleteObject:[_notesFetcher objectAtIndexPath:indexPath]];
}
NSError *error;
if (![_managedObjectContext save:&error]) {
DLog(#"Failed deleting note : %#", [error localizedDescription]);
}
}
}
The table view is using core data so the deletion of the rows occur in the appropriate controller's method as follows:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = (controller == _notesFilteredFetcher) ? self.searchDisplayController.searchResultsTableView : self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath forTable:tableView];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray
arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
I also noticed that when animation for delete does occur, all rows above the deleted row move a bit down and the rows under the deleted row move up until the empty space is covered.
If I look at the built-in mail app and delete some rows (i.e. mails) it does seem to animate as expected every time and ONLY the rows below the deleted row are moved up to cover the empty space (the rows above the deleted row do not move).
Any ideas ?
UPDATE:
I have noticed that animation does occur only for the last row. If I delete any other row no animation occurs but if I delete the last row animation does work.
If someone bumps into the same problem :
I noticed that I called reloadData method of the table view right after the call to endUpdates of the same table view as I needed to update the cell's internal indices.
This will cause the animation to stop immediately. Removing the reloadData method solved the animation issue.

Error message deleting the last row of a section using NSFetchedResultsController

I've got an UITableView that uses an NSFetchedResultsController as it's data source. I use one of the fields for my sections. I enabled row deletion (using the swipe gesture). Works just fine.
The problem comes up when I delete the last row of a section. It does not crash on me but the console shows the following message:
CoreData: error: Serious application error. An exception was caught
from the delegate of NSFetchedResultsController during a call to
-controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section
after the update (1) must be equal to the number of rows contained in
that section before the update (1), plus or minus the number of rows
inserted or deleted from that section (0 inserted, 1 deleted) and plus
or minus the number of rows moved into or out of that section (0 moved
in, 0 moved out). with userInfo (null)
This is all pretty basic stuff:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[[self tableView] beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
…
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
…
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[[self tableView] endUpdates];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
FHClass *foo = [[self fetchedResultsController] objectAtIndexPath:indexPath];;
[[self managedObjectContext] deleteObject:meal];
[self saveContext];
}
}
If I check -numberOfRowsInSection: I see that the number is steadily decreasing when deleting rows. Even to the point where it says 0.
What did I miss? Any hints? Stupid mistake on my part? ;-)
You should implement controller:didChangeSection:atIndex:forChangeType: to deal with section changes and then request the table view to deleteSections:.

Delete in NSFetchedResultsController does not persist until after restart

Even though I have worked with NSFetchedResultsController dozens of times, I have run into a problem I can't seem to find the cause of.
I have a UIViewController with a NSFetchedResultsController that manages the data source of a table view. Whenever I delete an object in the NSFetchedResultsController, the table view is updates by the fetched results controller, but when I save the NSManagedObjectContext (to commit the delete), an insert is triggered for the object that was just deleted. The net result is that the object is not removed from the table view.
The odd thing is that when the application is restarted, the object that was supposed to be deleted is actually deleted from the store and doesn't show up in the table view.
To be clear, I use only one managed object context, which is only accessed on the main thread. In other words, the setup is quite simple.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Fetch Store
CCDStore *store = [self.fetchedResultsController objectAtIndexPath:indexPath];
// Delete Store
[self.fetchedResultsController.managedObjectContext deleteObject:store];
// Save Changes
NSError *error = nil;
[self.fetchedResultsController.managedObjectContext save:&error];
if (error) {
NSLog(#"Error %# with user info %#.", error, error.userInfo);
}
}
}
Even though the deleted object is inserted in the fetched results controller, the object is turned into a fault, but I don't understand why it is added to the fetched results controller in the first place.
No exceptions or errors are thrown. I have performed several checks such as calling isDeleted and validateForDelete on the object to find the cause, but I have not become any wiser.
EDIT:
I am pretty sure that the fetched results controller's delegate methods are not the culprit, but for completeness I have added the implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
[self updateView];
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: {
[self configureCell:(CCDStoreCell *)[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
}
case NSFetchedResultsChangeMove: {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
default: {
break;
}
}
}

Resources