UITableView row delete crashes app - ios

I have a Core Data based table view that has a simple delete method. It is straightforward code that can be found in countless tutorials, however, it crashes the app with the following message:
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 (2) must be equal to the number of rows contained in that section before the update (2), 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).'
The delete method is as follows:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// get garden to delete, delete it through Core Data
Garden *gardenToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Garden name: %#", gardenToDelete.gardenName);
[self.managedObjectContext deleteObject:gardenToDelete];
[self.managedObjectContext save:nil];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
[self.tableView endUpdates];
}
}
I have spent hours rearranging the code, removing the beginUpdate and endUpdate calls, checking other solutions. Help is greatly appreciated.

You just need to delete the object from your NSFetchedResultsController, the fetched results controller will then update the table view assuming you've implemented the delegate methods as described in the docs. You shouldn't touch the table view rows, the NSFetchedResultsControllerDelegate does all of that.
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// deleting cell from fetched results contoller
Garden *gardenToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Garden name: %#", gardenToDelete.gardenName);
[self.managedObjectContext deleteObject:gardenToDelete];
[self.managedObjectContext save:nil];
}
}

Do you implement - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section with a return value with NSFetchedResultsController? After you do save, self.fetchedResultsController will refresh, but make sure all of the dataSource of tableView return value is depended on self.fetchedResultsController.

Do you implement these methods:
- (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;
// my comment: mapIndexPathFromFetchResultsController function, I just use to get indexPath. You can remove it
indexPath = [self mapIndexPathFromFetchResultsController:indexPath];
newIndexPath = [self mapIndexPathFromFetchResultsController:newIndexPath];
switch(type) {
case NSFetchedResultsChangeInsert:
{
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeDelete:
{
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeUpdate:
{
UITableViewCell *cell;
cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell != nil)
{
// my comment: just use the same with cellForRowAtIndexPath
// - (void)syncViewConfigureCell:(UITableViewCell *)cell {
// cell.textLabel.text = #""; etc. }
[self syncViewConfigureCell:cell];
}
break;
}
case NSFetchedResultsChangeMove:
{
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[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];
}

Related

Edit TableView containing NSFetchedResults

When deleting an object from my Core Data that is fetched using NSFetchedResultsController and displayed in a TableView, it does not update the table. It deletes the object just fine however the row remains there until I swap views and return. I have noticed that this issue has only started happening since iOS8 but could be wrong. Below is my code:
#pragma mark - Fetched Results Controller Delegate
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext * context = [self managedObjectContext];
entity * rowToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
[context deleteObject:rowToDelete];
//check that the row has deleted data
NSLog(#"Shhiiiiiiiiii...... You done did delete a row...");
NSError * error = nil;
if (![context save:&error]){
NSLog(#"Error: %#", error);
}
//causes a crash
//[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationRight];
//DOES NOT UPDATE THE TABLE
[self.tableView reloadData];
}
}
I use all the normall delegates like so:
- (void) controllerWillChangeContent:(NSFetchedResultsController *)controller{
[self.tableView beginUpdates];
}
- (void) controllerDidChangeContent:(NSFetchedResultsController *)controller{
[self.tableView endUpdates];
}
- (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:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeUpdate:{
entity * details = [self.fetchedResultsController objectAtIndexPath:indexPath];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
cell.textLabel.text = details.detailString;
}
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.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:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeMove:
NSLog(#"A table item was moved");
break;
case NSFetchedResultsChangeUpdate:
NSLog(#"A table item was updated");
break;
}
}
I have searched on Stack and find the generic response "You need to use [self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationRight];". This does not work.
Thanks in advance for any help. If I do not respond immediately it's because I am taking a breather or asleep ;-)
NSFetchedResultsController should be able to communicate all the changes for you.
Check that the update functions are being called (i.e. narrow down where the failure might be)
Check that the NSFetchedResultsController instance has the delegate set
Check that you are working the the same, or connected, context (i.e. check that there is even a chance of the notification propagating)
I'm not sure if you can ignore re-ordering (might depend on your approach) but imagine that [object A, row 1] is swapped with [object B, row 2], then object B is deleted, how does the system know which table row to delete (unless you do something extra with the information)
Manually deleting a row from the table will cause a crash as the data source will be out of line with the table -- thus, causing all manner of confusion. The delegate methods for the results are there to enable the synchronisation of the actual results with those shown in the table.
I cut the following out of a working demo (although modified a bunch of stuff on the way). It works and receives changes, and updates the table. There are a few blog posts only a google away that will help too.
- (void)viewDidLoad {
[super viewDidLoad];
_fetchedResultsController = /* my fetched results controller... */;
_fetchedResultsController.delegate = self;
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSInteger count = [[_fetchedResultsController sections] count];
return count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
NSInteger count = [sectionInfo numberOfObjects];
return count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self configureCell:nil atIndexPath:indexPath];
}
- (UITableViewCell *)configureCell:(UITableViewCell*)cell atIndexPath:(NSIndexPath*)indexPath {
id obj = [self.fetchedResultsController objectAtIndexPath:indexPath];
if(!cell) {
cell = [self.tableView dequeueReusableCellWithIdentifier:#"MyTableViewCell" forIndexPath:indexPath];
}
cell.textLabel.text = obj.someProperty;
return cell;
}
#pragma mark NSFetechResults delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo
atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
default:
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:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath]
atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
I stupidly placed
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
At the beginning before the actions could update. Rookie mistake. Hope it helps anyone else who falls for the same thing.

Table with NSFetchedResultsController move row with adding section

I have UITableView with NSFetchedResultsController
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:[self dataFetchRequest]
managedObjectContext:[MLCoreDataService sharedInstance].managedObjectContext
sectionNameKeyPath:#"checked"
cacheName:nil];
aFetchedResultsController.delegate = self;
I've implemented
- (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:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[(MLItemTableViewCell*)[tableView cellForRowAtIndexPath:indexPath] setItem:anObject];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
break;
}
[self.tableView reloadData];
}
When I change "checked" property of entity in my row I got a call to
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
with type NSFetchedResultsChangeMove
All is right when I have the same sections amount before editing and after.
But when I have only one section and after editing row should go to the next section I got the following error
2014-07-27 17:37:43.384 mlist[34340:60b] CoreData: error: Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification. Invalid update: invalid number of sections. The number of sections contained in the table view after the update (2) must be equal to the number of sections contained in the table view before the update (1), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted). with userInfo (null)
2014-07-27 17:37:43.420 mlist[34340:60b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (2) must be equal to the number of sections contained in the table view before the update (1), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).'
When I try to insert of delete on didChangeObject... method with type NSFetchedResultsChangeMove I have the same error with numbers of rows in sections.
Is there common way to resolve it? And what the right way to do it?
Problem resolved by custom call of
[self.tableView beginUpdates];
[self.tableView endUpdates];
Whole code of NSFetchedResultsControllerDelegate
- (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:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeDelete:{
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
case NSFetchedResultsChangeUpdate:{
[(MLItemTableViewCell*)[tableView cellForRowAtIndexPath:indexPath] setItem:anObject];
}
break;
case NSFetchedResultsChangeMove:{
if (_sectionsChanged) {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
_sectionsChanged = NO;
} else {
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
}
}
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;
}
_sectionsChanged = YES;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}

UITableView - No Items Label - using third cell like Notes App - Crashes when inserting the first record and deleting the last record

I would like to implement the same behavior as was done in iOS6 with the notes App, perhaps the same is done with iOS7 I can't remember.
To display the message "No Notes" in the third cell, I have done that just fine, however when I add the first record or delete the last record it crashes.
I don't want to use the table header or footer, I would like to do it the same way as done in the Notes app please.
Similar to this question
UITableView deleteRowsAtIndexPath crash when delete last record
so the problem is it crashes when inserting the first record or deleting the last record. The error message is the same:-
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 (3), 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)
I understand the problem and why it happens but I have tried too many different things to resolve it but I didn't know how.
Can someone kindly please help me. Thank and much appreciated.
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (self.myModel.count == 0)
{
return 3;
}
return self.myModel.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"Identifier" forIndexPath:indexPath];
if (self.allNotes.count == 0)
{
cell = [self initilizeNoItemsCell:cell forIndexPath:indexPath];
}
else
{
cell = [self initilizeMyObjectCell:cell forIndexPath:indexPath];
}
return cell;
}
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type)
{
case NSFetchedResultsChangeInsert:
//crashes here when inserting the first record
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[self.tableView reloadData];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)tableView:(UITableView *)tableViewObj commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
//Crashes here also when deleting the last record
[MyModel deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];
NSError *error;
if (![MyModel save:&error])
{
NSLog(#"Failed to delete - error: %#", [error localizedDescription]);
}
}
else if (editingStyle == UITableViewCellEditingStyleInsert)
{
}
}
Ho do I get rid of the fake 3 rows before I add a new record?
Update 1:-
When inserting a second record.
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 (2) 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, 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)
When deleting a record (not the last record)
CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. attempt to insert row 1 into section 0, but there are only 1 rows in section 0 after the update with userInfo (null)
After getting Paul's answer, its getting me closer. It crashes when I try to add a second record. Also when deleting anything but the last record.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
NSArray *emptyIndexPaths = [NSArray arrayWithObjects:
[NSIndexPath indexPathForRow:0 inSection:0],
[NSIndexPath indexPathForRow:1 inSection:0],
[NSIndexPath indexPathForRow:2 inSection:0],
nil];
switch(type)
{
case NSFetchedResultsChangeInsert:
if (self.allNotes.count == 1)
{
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:emptyIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
break;
case NSFetchedResultsChangeDelete:
if (self.allNotes.count == 1)
{
[tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:emptyIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
break;
case NSFetchedResultsChangeUpdate:
[self.tableView reloadData];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
Thanks,
When you delete the last row then the expected row count will be 0 (1 row - 1 row deleted = 0), but your numberOfRowsInSection will return 3 - because self.myModel.count is now 0.
A similar thing will occur when you insert the first row - 4 is the expected row count (3 + 1), but 1 is the value from numberOfRowsInSection.
You need -
NSArray *emptyIndexPaths = [NSArray arrayWithObjects:
[NSIndexPath indexPathForRow:0 inSection:0],
[NSIndexPath indexPathForRow:1 inSection:0],
[NSIndexPath indexPathForRow:2 inSection:0],
nil];
case NSFetchedResultsChangeInsert:
[tableView beginUpdates];
if (self.myModel.count == 1) {
[tableView deleteRowsAtIndexPaths:emptyIndexPaths withRowAnimation:UITableViewRowAnimationFade];
}
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
break;
case NSFetchedResultsChangeDelete:
[tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
if (self.myModel.count == 0) {
[tableView insertRowsAtIndexPaths:emptyIndexPaths withRowAnimation:UITableViewRowAnimationFade];
}
[tableView endUpdates];
break;

UITableView doesn't display the cell correctly when inserting in the first row with animations

My app has an UITableViewController as the root controller and a modal view to add a row to this table. I am using CoreData, so I get the data from a NSFetchedResultsController. Anyway, the problem is in the manage of the UITableView.
Each insertion makes TableView add a new cell. Everything works fine except if the cell is added in the first row. In this case its content is not displayed. The cell is blank displayed. If I tap in the cell or I scroll the table in a way the cell must be reloaded it does show its content.
I am using animations so I do [tableView beginUpdates] and [tableView endUpdates] as Apple docs said, instead of [tableView reloadData]. If I do [tableView reloadData] all works fine.
I have checked it and same code is runned for every row. It is a problem about the way the cells are displayed.
I think the problem is about "theory" of animations in TableViews and you won't probably need it, but there is the relevant code in my UITableViewController:
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
Customer *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = managedObject.name;
}
#pragma mark - Table view data source
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
#pragma mark - FetchedResultsController delegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
// [self.tableView reloadData]; this make all works OK but without animations
}
- (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;
}
}
In your switch statement for each type of change, why not add:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
You will have to vary that code slightly depending on the type of change, but this would eliminate the need for beginUpdates and endUpdates.

tableView:cellForRowAtIndexPath called with nil indexPath after deleting item

I have a rather vanilla UITableView managed by an NSFetchedResultsController to display all instances of a given Core Data entity.
When the user deletes an entry in the table view by swiping over it, tableView:cellForRowAtIndexPath: eventually gets called on my UITableViewController with a nil indexPath. Since I had not expected it to be called with a nil indexPath, the app crashes.
I can work around the crash by checking for that nil value and then returning an empty cell. This seems to work, but I still worry that I may have handled something wrong. Any ideas? Has anybody ever seen tableView:cellForRowAtIndexPath: called with a nil indexPath?
Note that this only happens when the user deletes from the table view by swiping over the cell. When deleting an item using the table view editing mode, it doesn't happen. What would be different between the two ways to delete a cell?
So is it really an OK situation to get a nil indexPath in a table view delegate method?
My view controller code is really standard. Here is the deletion:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.moc deleteObject:managedObject];
NSError *error = NULL;
Boolean success = [self.moc save:&error];
if (!success) { <snip> }
// actual row deletion from table view will be handle from Fetched Result Controller delegate
// [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
} else { <snip> }
}
This will lead to the NSFetchedResultsController delegate method being called:
- (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 NSFetchedResultsChangeInsert: <snip> break;
case NSFetchedResultsChangeUpdate: <snip> break;
case NSFetchedResultsChangeMove: <snip> break;
}
}
And of course, the data source methods are handled by the NSFetchedResultsController, e.g.:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
Many thanks.
It seems like you are deleting the indexPath from table but table data source is not updating.
Did you verify the data source udation process by NSFetchedResultsController is correctly updationg the table data source.?
I would do like this, since you are populating the table directly from your managed context, why not on delete first delete the object from the managed context and then imediately update the table from the context using reloadData. But using your approach i think you need to add beginUpdates and endUpdates:
#pragma mark -
#pragma mark Fetched results controller delegate
- (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 *tableViews = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableViews insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableViews deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[_delegate configureCell:[tableViews cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableViews deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableViews insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionMiddle
animated:NO];
}
I use CoreData in table views and allow the users to delete records and update them all the time, I have them in 4 apps without encountering the problem you mentioned.
I have these FetchedResultsController Delegate Method in my code
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.myTableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller has sent all current change notifications, so tell the table view to process all updates.
[self.myTableView endUpdates];
}

Resources