Getting An Error When Deleting Cell From Table - iphone

When I delete a cell from my table, i am getting the following error:
Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1448.89/UITableView.m:995
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 (5) must be equal to the
number of rows contained in that
section before the update (5), plus or
minus the number of rows inserted or
deleted from that section (0 inserted,
1 deleted). with userInfo (null)
Here is the relevant code:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// Delete the managed object for the given index path
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
NSLog(#"fetched results : \n%#\n",[self.fetchedResultsController fetchedObjects]);
// Commit the change.
NSError *error = nil;
// Update the array and table view.
if (![managedObjectContext save:&error])
{
// Handle the error.
}
//[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
}
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
#pragma mark - Fetched results controller delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.resultsTableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.resultsTableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.resultsTableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.resultsTableView;
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];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.resultsTableView endUpdates];
}
I tried removing [self.setsTableView reloadData]; from the last method but that did not work either.

seems like you're deleting the cell from the table, but not deleting the object from the table datasource, which creates an inconsistency.
try adding something like:
[[[self.fetchedResultsController sections] objectAtIndex:indexPath.section] removeObjectAtIndex:indexPath.row];
when you use tableView:deleteRowsAtIndexes
the number returned by numberOfRowsInSection: must by the same as the number of rows in your tableView, minus/plus the ones you've deleted/added

Change self.fetchedResultsController to fetchedResultsController in all the above methods. Just keep self.fetchedResultsController only in method
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[self.fetchedResultsController sections] count];
}
//--Try to keep managedObjectContext as a member variable and use the following setter //method.
- (NSManagedObjectContext *)managedObjectContext {
if (managedObjectContext != nil)
{
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [(AppDelegate *)[[UIApplication sharedApplication] delegate] persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return managedObjectContext;
}

Related

NSFetchedResultsController's delegate method handling

I have UITableView with NSFetchedResultsController attached to it. FRC's delegate methods work perfectly and everything is nice, but my cells have custom backgrounds that depend on the position of the cell:
1. on top of the UITableView (background with rounded corners on top)
2. in the middle (background with no rounded corners)
3. on the bottom (background with rounded corners on the bottom)
4. single cell (all corners are rounded).
In my cell configuration method I calculate a position of the cell and I'm setting an appropriate background view to it. Everything works fine, but there is a problem in the implementation of FRC's delegate method:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.agenciesTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
}
case NSFetchedResultsChangeDelete:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
break;
}
case NSFetchedResultsChangeUpdate:
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeMove:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
};
}
As you can see it is completely common, but there is one issue:
If the cell gets inserted to the top of UITableView, its configuration method (cellForRowAtIndexPath) gets called and it draws correctly (with rounded top corners) but the cell that is under it (that was previously on the top) remains with rounded top corners and I need to redraw it somehow.
-=EDITED=-
I changed FRC's delegate method to reflect the update logic:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.agenciesTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
//if we inserting a cell not to the middle of table view
//we need to update the previous cell (if we are inserting to the end)
// or the next cell (if we are inserting to the beggining)
if ([self controller:controller positionForCellAtIndexPath:newIndexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row+1) inSection:newIndexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row-1) inSection:newIndexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:prevIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:nextIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
};
break;
}
case NSFetchedResultsChangeDelete:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
//if we deleting a cell not fromo the middle of table view
//we need to update the previous cell (if we are deleting from the end)
//or the next cell (if we are deleting from the beggining)
if ([self controller:controller positionForCellAtIndexPath:indexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(indexPath.row+1) inSection:indexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(indexPath.row-1) inSection:indexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:prevIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:nextIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
};
break;
}
case NSFetchedResultsChangeUpdate:
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
break;
}
case NSFetchedResultsChangeMove:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
if ([self controller:controller positionForCellAtIndexPath:indexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(indexPath.row+1) inSection:indexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(indexPath.row-1) inSection:indexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:prevIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:nextIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
};
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
if ([self controller:controller positionForCellAtIndexPath:newIndexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row+1) inSection:newIndexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row-1) inSection:newIndexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:prevIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:nextIndexPath] withRowAnimation:UITableViewRowAnimationNone];
};
};
break;
}
};
}
But in this case I'm getting an error:
2013-05-24 21:04:19.458 PharmaTouch[6994:fb03] *** Assertion failure in -[_UITableViewUpdateSupport _computeRowUpdates], /SourceCache/UIKit_Sim/UIKit- 1912.3/UITableViewSupport.m:386
2013-05-24 21:04:19.483 PharmaTouch[6994:fb03] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid table view update. The application has requested an update to the table view that is inconsistent with the state provided by the data source. with userInfo (null)
-=EDITED2=-
here's my controller:cellExistsAtIndexPath: method implementation. It is used for safety checks to avoid reloadRowsAtIndexPaths calls with indexPath arguments that is out of bounds.
-(BOOL)controller:(NSFetchedResultsController *)controller cellExistsAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row<0)
{
return NO;
};
NSInteger numberOfRows = 0;
NSArray *sections = controller.sections;
if(sections.count > 0)
{
id <NSFetchedResultsSectionInfo> sectionInfo = [sections objectAtIndex:indexPath.section];
numberOfRows = [sectionInfo numberOfObjects];
};
if (indexPath.row>(numberOfRows-1))
{
return NO;
}
else
{
return YES;
};
}
As you're using UITableViewRowAnimationNone you might as well just reloadData and then it will be done correctly because all rows will be refreshed.
Where you're using UITableViewRowAnimationFade, consider how much you value it...
You'd need to be a bit more comprehensive in your checks and reloading. In particular, you need to get the section info from the FRC for the section that's changing. If the incoming or out going row is the first or last, you need to refresh more than one row. Along the lines of:
if first is changing and count > 1, also refresh second
if last is changing and count > 1, also refresh first
I've solved the problem by adding a property #property (nonatomic, retain) NSMutableSet *indexPathsForRowsToReloadAfterFRCUpdates; to keep the rows that needs to be updated between controllerWillChangeContent: controllerDidChangeContent: method calls (in FRC delegate update cycle).
#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
self.indexPathsForRowsToReloadAfterFRCUpdates=[[NSMutableSet alloc] init];
[self.agenciesTableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
UITableView *tableView = self.agenciesTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeDelete:
{
[tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
};
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = self.agenciesTableView;
switch(type)
{
case NSFetchedResultsChangeInsert:
{
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
//if we inserting a cell not to the middle of table view
//we need to update the previous cell (if we are inserting to the end)
// or the next cell (if we are inserting to the beggining)
if ([self controller:controller positionForCellAtIndexPath:newIndexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row-1) inSection:newIndexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row+1) inSection:newIndexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:prevIndexPath];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:nextIndexPath];
};
};
break;
}
case NSFetchedResultsChangeDelete:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
//if we deleting a cell not fromo the middle of table view
//we need to update the previous cell (if we are deleting from the end)
//or the next cell (if we are deleting from the beggining)
if ([self controller:controller positionForCellAtIndexPath:indexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(indexPath.row) inSection:indexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(indexPath.row+1) inSection:indexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:prevIndexPath];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:nextIndexPath];
};
};
break;
}
case NSFetchedResultsChangeUpdate:
{
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
break;
}
case NSFetchedResultsChangeMove:
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
if ([self controller:controller positionForCellAtIndexPath:indexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(indexPath.row) inSection:indexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(indexPath.row+1) inSection:indexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:prevIndexPath];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:nextIndexPath];
};
};
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
if ([self controller:controller positionForCellAtIndexPath:newIndexPath]!=UITableViewCellPositionMiddle)
{
NSIndexPath *prevIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row-1) inSection:newIndexPath.section];
NSIndexPath *nextIndexPath=[NSIndexPath indexPathForRow:(newIndexPath.row+1) inSection:newIndexPath.section];
if ([self controller:controller cellExistsAtIndexPath:prevIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:prevIndexPath];
};
if ([self controller:controller cellExistsAtIndexPath:nextIndexPath])
{
[self.indexPathsForRowsToReloadAfterFRCUpdates addObject:nextIndexPath];
};
};
break;
}
};
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.agenciesTableView endUpdates];
if ([self.indexPathsForRowsToReloadAfterFRCUpdates count]>0)
{
[self.agenciesTableView reloadRowsAtIndexPaths:[[self.indexPathsForRowsToReloadAfterFRCUpdates allObjects] copy] withRowAnimation:UITableViewRowAnimationNone];
};
self.indexPathsForRowsToReloadAfterFRCUpdates=nil;
}

Core Data Books Example Add Custom Cell

I have based my app upon the Core Data Books example project Apple provide and I have made each cell a custom cell which which displays certain data, works magically.
However, now I am trying to add a second custom cell. It is a singular cell which will always be the first cell in the table, then all of the fetched results from core data go below that.
I have tried like so:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0)
{
static NSString *CellIdentifier = #"statsCell";
GuestStatsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[GuestStatsCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
//Configure the cell.
[self configureStatsCell:cell];
return cell;
}
else
{
static NSString *CellIdentifier = #"guestCell";
customGuestCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[customGuestCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell.
[self configureGuestCell:cell atIndexPath:indexPath];
return cell;
}
}
Along with some other changes to try and get things working. A few problems.
Firstly, the view loads fine, I get my custom cell at index 0 and then my core data cells load up below that. First problem, the very first core data cell isn't visible as its hidden behind my custom cell but if I tap it I do get the detail view for that core data entry. So I need to work out how I can stop it going behind that custom cell.
I have tried to remedy this, for example, it seems logical that this chunk:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
So, this is only setting enough cells for the core data records and doesn't account for that additional custom cell. So I try something as simple that this:
return [sectionInfo numberOfObjects] + 1;
Then this results in this line causing me to get a crash and index beyond bounds error:
GuestInfo *guest = [fetchedResultsController objectAtIndexPath:indexPath];
That line is from the configureCell method which setups the core data record cells. So Im pretty stumped as to what to do.
Next problem, when I edit an attribute of any core data record in the detail view, returning to the tableview my custom cell doesn't show any changes. It displays stats to do with the core data info. Only the other cells get updated.
ANy help would be much appreciated, and if you need to see anymore code or need more explanation let me know.
EDIT:
More code as requested to help me. This is the NSFetchedController code.
- (NSFetchedResultsController *)fetchedResultsController
{
if (fetchedResultsController != nil)
{
return fetchedResultsController;
}
// Create and configure a fetch request with the Book entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"GuestInfo" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Create the sort descriptors array.
NSSortDescriptor *lastNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"lastName" ascending:YES];
NSSortDescriptor *firstNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"firstName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:lastNameDescriptor, firstNameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Create and initialize the fetch results controller.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:#"displayOrder" cacheName:#"Root"];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
// Memory management.
//No releasing with ARC!
return fetchedResultsController;
}
/**
Delegate methods of NSFetchedResultsController to respond to additions, removals and so on.
*/
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[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 NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureGuestCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
[self.tableView reloadData];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}
I'm implementing the same function and using offset with indexPath #Toro's solution work just fine. Don't know if there is a better way or not here is my adjustment for those interested.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects] + 1;
}
Add one extra row for my custom cell, my fetched result have no section so I this work just fine.
- (NSIndexPath *)adjustedIndexPath:(NSIndexPath *)indexPath
{
NSIndexPath *newPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:indexPath.section];
return newPath;
}
private method for adjust indexpath to retrieve right object from NsFetchedResultController.
Here are method that I apply adjustedIndexPath:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

tableView endUpdates crashes after change in fetchedResults

I have an app which shows a custom no-data-yet cell when the user first launches the app.
When the user makes the first entry the fetchedResults of my fetchedResultsController gets updated, which causes the no-data-yet cell to be deleted and a data cell to be inserted.
this used to work in the past (iPhone 3.x). Now on iOS 4.2, it results in a crash after endUpdates gets called. There is no exception information or any kind of intelligible stack trace. I only know the crash is caused in _CFTypeCollectionRetain (possibly trying to retain a NULL)
Any ideas how to proceed?
here is the relevant code:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"starting updates");
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tv = self.tableView;
[tv reloadData];
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"insert");
if ([self.tableView numberOfRowsInSection:newIndexPath.section] == 1 &&
[[self.fetchedResultsController fetchedObjects] count] == 1)
{
NSLog(#"reloading row %d", newIndexPath.row);
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else {
NSLog(#"inserting new row %d", newIndexPath.row);
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeDelete:
NSLog(#"delete");
if ([self.tableView numberOfRowsInSection:newIndexPath.section] == 0 &&
[[self.fetchedResultsController fetchedObjects] count] == 0)
{
NSLog(#"reloading row %d", newIndexPath.row);
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tv insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else {
NSLog(#"deleting row %d", newIndexPath.row);
[tv deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"finishing updates");
[self.tableView endUpdates];
}
In the delegate method
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
you handle insertions and deletions. For insertions indexPath is nil. And for deletions newIndexPath is nil. So you are not allowed to access the newIndexPath in case of type== NSFetchedResultsChangeDelete.
Besides the call to reloadData is not necessary here.

moveRowAtIndexPath in UITableView causes incorrect animation

I have a simple UITableView Controller that shows CoreData. I'm trying to implement - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; and having trouble with the animation. The Core Data store gets updated, but the animation is not working.
How can I get the animation to correctly reflect the changes that are happening to the core data objects?
For example:
Initial order:
After item 2 to the top:
or, Initial Order:
After moving item 1 to position 3:
Here's the relevant code:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
//this implementation is from this tutorial: http://www.cimgf.com/2010/06/05/re-ordering-nsfetchedresultscontroller/
NSMutableArray *things = [[fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving.
NSManagedObject *thing = [fetchedResultsController objectAtIndexPath:fromIndexPath];
// Remove the object we're moving from the array.
[things removeObject:thing];
// Now re-insert it at the destination.
[things insertObject:thing atIndex:[toIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 0;
for (NSManagedObject *mo in things)
{
[mo setValue:[NSNumber numberWithInt:i++] forKey:#"order"];
}
NSLog(#"things: %#", things);
[things release], things = nil;
[managedObjectContext save:nil];
}
and the delegate:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
NSLog(#"didChangeObject:");
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"ResultsChangeInsert:");
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
The problem was caused by the delegate interfering with my reordering implementation.
I added a bool before saving my ManagedObjectContext:
reordering = YES;
[managedObjectContext save:nil];
and used it to skip out of any of the FetchedResultsController Delegate functions. Not the best solution, but it works for this implementation. I'd be grateful for any comments / answers explaining why this happened.
/**
Delegate methods of NSFetchedResultsController to respond to additions, removals and so on.
*/
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
if (!reordering) {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
NSLog(#"controllerWillChangeContent:");
[self.tableView beginUpdates];
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
if (!reordering) {
NSLog(#"didChangeObject:");
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
NSLog(#"ResultsChangeInsert:");
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
/*case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;*/
}
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
if (!reordering) {
NSLog(#"didChangeSelection:");
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
if (!reordering) {
NSLog(#"didChangeContent:");
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.tableView endUpdates];
}else {
reordering = NO;
}
}
I had the same problem, and I fixed it by moving the saving code to a "save" place. i.e.:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
[super setEditing:editing animated:animated];
if (!editing) {
//save here
}
}
I think it's understandable that things would be messed up if you save in the middle of moving things around. And -(void)setEditing seems to be a good place to do the saving.
btw, thanks for pointing out the cause of the problem!

How to implement delayed/batched table view update from NSFetchedResultsControllerDelegate?

The documentation says:
You should consider carefully whether you want to update the table view as each change is made. If a large number of modifications are made simultaneously—for example, if you are reading data from a background thread— /.../ you could just implement controllerDidChangeContent: (which is sent to the delegate when all pending changes have been processed) to reload the table view.
This is exactly what I'm doing: I'm processing incoming changes in a background thread with a different ManagedObjectContext, and merge the results into the main thread MOC with mergeChangesFromContextDidSaveNotification:. So far so good.
I chose to not implement controller:didChangeObject:... and would instead like to do the batched update that the document suggests.
Question/problem: the document doesn't elaborate how to actually implement the batched update? Should I just call [tableview reloadData] in controllerDidChangeContent: or is there a less intrusive way that saves me from a full reload?
One thought I have: I could take note of mergeChangesFrom... notification that contains the changed objects, figure out their indexpaths, and just call tableview:ReloadRowsAtIndexPaths: for them. But is there any authoritative info, recommendations or examples? Or just [tableview reloadData]?
(Aside: controller:didChangeObject:... started behaving really erratically when it received a set of batched updates, even though the same updating code [that I now put in background thread] was fine before when it was running on the main thread, but of course locking up the UI.)
I would just call reloadData in controllerDidChangeContent:.
For animating individual changes to the table, Apple's boiler plate code (iOS SDK 4.3.x) looks like this:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
UITableView *tableView = 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];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
I believe this is what you're looking for:
Instead of calling
deleteSections
insertSections
reloadSections
deleteRowsAtIndexPaths
insertRowsAtIndexPaths
reloadRowsAtIndexPaths
as the changes are coming in at controller:didChangeObject:atIndexPath:forChangeType:newIndexPath, collect the indexPaths and sections in properties. Then execute the batch in controllerDidChangeContent:.
Ref: http://www.fruitstandsoftware.com/blog/2013/02/19/uitableview-and-nsfetchedresultscontroller-updates-done-right/
Edit
Here is a slightly different, more contrived, but also more compact, version of the approach mentioned in the link:
#property (nonatomic, strong) NSMutableArray *sectionChanges;
#property (nonatomic, strong) NSMutableArray *objectChanges;
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
atIndex:(NSUInteger)sectionIndex
forChangeType:(NSFetchedResultsChangeType)type
{
NSMutableDictionary *sectionChange = [NSMutableDictionary new];
switch(type)
{
case NSFetchedResultsChangeInsert:
case NSFetchedResultsChangeDelete:
case NSFetchedResultsChangeUpdate:
sectionChange[#(type)] = #(sectionIndex);
break;
}
[self.sectionChanges addObject:sectionChange];
}
- (void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
NSMutableDictionary *objectChange = [NSMutableDictionary new];
switch(type)
{
case NSFetchedResultsChangeInsert:
objectChange[#(type)] = newIndexPath;
break;
case NSFetchedResultsChangeDelete:
case NSFetchedResultsChangeUpdate:
objectChange[#(type)] = indexPath;
break;
case NSFetchedResultsChangeMove:
objectChange[#(type)] = #[indexPath, newIndexPath];
break;
}
[self.objectChanges addObject:objectChange];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
UITableView *tableView = self.tableView;
NSUInteger totalChanges = self.sectionChanges.count + self.objectChanges.count;
if (totalChanges > 0)
{
[tableView beginUpdates];
if (self.sectionChanges.count > 0)
{
for (NSDictionary *change in self.sectionChanges)
{
[change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch (type)
{
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadSections:[NSIndexSet indexSetWithIndex:[obj unsignedIntegerValue]] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}];
}
}
else if (self.objectChanges > 0)
{
NSMutableArray *indexPathsForUpdatedObjects = [NSMutableArray new];
for (NSDictionary *change in self.objectChanges)
{
[change enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, id obj, BOOL *stop) {
NSFetchedResultsChangeType type = [key unsignedIntegerValue];
switch (type)
{
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[indexPathsForUpdatedObjects addObject:obj];
//[tableView reloadRowsAtIndexPaths:#[obj] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:obj[0] toIndexPath:obj[1]];
break;
}
}];
}
[tableView endUpdates];
for (NSIndexPath *indexPath in indexPathsForUpdatedObjects)
if ([tableView.indexPathsForVisibleRows containsObject:indexPath])
[self updateCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
}
[self.sectionChanges removeAllObjects];
[self.objectChanges removeAllObjects];
}
else if (totalChanges < 1)
{
[tableView reloadData];
}
}
- (NSMutableArray *)sectionChanges
{
if (!_sectionChanges)
_sectionChanges = [NSMutableArray new];
return _sectionChanges;
}
- (NSMutableArray *)objectChanges
{
if (!_objectChanges)
_objectChanges = [NSMutableArray new];
return _objectChanges;
}
Note that this solution is not perfect in that it does not update objects if there are changes to sections and those objects are in a section that is not updated. Should be easy to fix (and is irrelevant for many applications).
You could note the last index path in your datasource & then do this -
NSIndexPath *indexPath = nil;
NSMutableArray *newResults = [[NSMutableArray alloc] init];
if(gLastResultIndex < [self.dataSource count])
{
for(int i=(gLastResultIndex); i<[self.dataSource count]; i++)
{
indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[newResults addObject:indexPath];
}
[self.table beginUpdates];
[self.table insertRowsAtIndexPaths:newResults withRowAnimation:UITableViewRowAnimationNone];
[self.table endUpdates];
}
[newResults release];
This is selectively adding new rows to your existing UITableView. reload table reloads all cells in your table which you might not need. I use reload table only when the entire datasource changes...