I'm using Matt Gallagher's GenericTableViewController idea for controlling my UITableViews. My datasource is a NSFetchedResultsController.
http://cocoawithlove.com/2008/12/heterogeneous-cells-in.html
Everything is working fine, until I try to delete a cell.
I have the following code in my View Controller:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object.
NSManagedObjectContext *context = [wineryController managedObjectContext];
[context deleteObject:[wineryController objectAtIndexPath:indexPath]];
NSError *error;
if (![context save:&error]) {
// Handle the error.
}
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
The final line crashes with the rather verbose explanation in the console:
*** 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 (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).'
OK, I understand what it is saying... a row is not getting deleted (I would assume) because I'm not forwarding some message to the right place (since I have moved some code from its 'normal' location)... anyone have any idea which one? I am totally stumped on this one.
Well, bah. I just found this answer, which is not the same, but got me headed in the right direction. I'll leave this here for anyone in the future having similar troubles.
The key is to wrap the deleteRowsAtIndexPaths with begin and end tags, and force the model to update within the same block, resulting in:
[tableView beginUpdates];
[self constructTableGroups];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
This caused the issue to go away, and the animations to work just perfectly.
Related
I am attempting to delete a UITableViewCell while showing a progress HUD (in this case MBProgressHUD). This is necessary, as the Core Data entity being deleted is relatively large. When I run this code I get the following error 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 code being executed is:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.tableView beginUpdates];
[SVProgressHUD showWithStatus:#"Deleting..." maskType:SVProgressHUDMaskTypeGradient];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
Garden *gardenToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Deleting garden '%#'", gardenToDelete.gardenName);
[self.managedObjectContext deleteObject:gardenToDelete];
[self.managedObjectContext save:nil];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Dismissing progress HUD");
NSLog(#"delete animation");
NSLog(#"deleting row");
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
NSLog(#"performFetch");
[self performFetch];
[SVProgressHUD dismiss];
});
});
[self.tableView endUpdates];
}
}
When it runs, I see the HUD appear, then the app proceeds to hang.
I am sure that this has to do with the structure of my multitasking.
managedObjectContext is not thread-safe. You need to create it for each thread.
When you call dispatch_async the first time, you're detaching that thread and allowing the method to continue to execute. This means that [self.tableView endUpdates]; is being called before [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
Instead, place your calls to [self.tableView beginUpdates]; and [self.tableView endUpdates]; inside your second call to dispatch_async when you reenter the main thread.
At first my table view is empty, and then you can add your own cells to it. When you delete those cells, everything works fine. However, if you delete the last cell, then my NSMutableArray has no objects in it, and I get this error in my console (also, I'm using Core Data to save the cells):
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[_PFBatchFaultingArray objectAtIndex:]: index (123150308) beyond bounds (1)'
I also tried putting in this line of code, but I still get the same results:
//arr is my mutable array
if ([arr count] == 0) {
NSLog(#"No Cells");
}
This is how I delete an object from the table view:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
[arr removeObjectAtIndex:0];
[context deleteObject:[arr objectAtIndex:0]];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
How would I solve this problem?
Ok.
There are two problems I have found in you code.
1- Why you are removing every time object at Index 0 ?
2- After removing an object from array[arr removeObjectAtIndex:0]; than from the same array of index you are passing an object to core Data to delete it
[context deleteObject:[arr objectAtIndex:0]];
This might be the problem.
This will surely help you.
Use this:
[context deleteObject:[arr objectAtIndex:indexPath.row]];
[arr removeObjectAtIndex:indexPath.row];
Thanks :)
If you look at the error message, the reason your code is failing is because some of your code is looking for a nonexistent index of 123150308. Without seeing your full code, it is impossible what exactly is wrong, but there is a simple fix.
A good way to solve the issue of an exception in code where the exception is "expected behavior" is to use #try blocks. This is your tableView method with #try blocks in place:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
#try {
[arr removeObjectAtIndex:0];
[context deleteObject:[arr objectAtIndex:0]];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
#catch (NSRangeException *exception) {
// Something was out of range; put your code to handle this case here
}
}
}
However, without the context of the rest of your app, it is impossible to tell if this is the error. If you try this and it doesn't work, then the error is deeper in your application
Ok so I was in the final stages of testing of my app and now I have run into a problem that has stumped me.
The process is as follows:
1) Change via coredata to "Main table" which is linked to myTableView in the firstViewController
2) As part of the above code I am also deleting all prior objects and saving a string to "Sync Log Table". As a result this table should be left with one object, a string e.g. "Mon 20:33".
Note: this is a newly implemented feature and I believe it to be the source of the problem, I want it as I am using it to populate a label on all devices to show the last synced data, in other words giving the user an easy check that all devices have the same information and are up to date
3) Both changes are then saved by calling [managedObjectContext save:&error];
4) On a second device everything is working fine and I can make updates to 'Main Table" which show up on myTableView after calling the code to reload.
Note I had 3 rows showing in myTableView on the second device and the change which was made in step 1 to "Main Table" will not change the tableview on the second device due to the predicate which is filtering results.
Then the problem starts
5) The second device begins to receive the changes via iCloud and the delegate method:
- (void)mergeiCloudChanges:(NSNotification*)note forContext:(NSManagedObjectContext*)moc {
NSLog(#"AppDelegate Merge icloud changes for context");
//***Specifically the line below!!
[moc mergeChangesFromContextDidSaveNotification:note];
NSNotification* refreshNotification = [NSNotification notificationWithName:#"RefreshAllViews" object:self userInfo:[note userInfo]];
[[NSNotificationCenter defaultCenter] postNotification:refreshNotification];
}
The line indicated above in the code kicks off the following methods in my firstViewController
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
NSLog(#"I'm inside the method Controller Will Change Context");
[self.myTableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
NSLog(#"I'm inside the method Controller did Change Object");
UITableView *tableView = self.myTableView;
switch(type) {
// And enters in this line here
case NSFetchedResultsChangeInsert:
[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;
}
}
Then I get the error:
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 (3) 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)
After receiving this error, myTableView is no longer responsive, it won't update or respond to swipe to delete for instance.
Basically from my understanding, the objects which are getting merged from the "Sync Log table" are being inserted (when they shouldn't be) into the myTableView delegate methods which should only receive objects from "main table" thus throwing out the count and causing the error.
Any idea how to resolve this in my circumstances? Any help would be greatly appreciated
Your tableview objects (ie, fetch controller, or NSArray, whatever) need to be updated before you call insertRowsAt… or deleteRowsAt… on the tableview itself. The tableview is telling you that the numbers don't add up for what it expects - if you are using a fetch controller, perhaps after you call [moc mergeChangesFromContextDidSaveNotification:note], you need to send a save message to the moc, or force a refresh for your table view's data objects.
I can't figure out why my tableView isn't updating after I tap the delete button.
Once I click it, the table view "freezes". If I click another row, so that the tableview goes to another level of the hierarchy and click back, I can see that the item has been deleted and everything works fine.
Here is my code:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
//[tableView beginUpdates];
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// Do whatever data deletion you need to do...
[tableView beginUpdates];
NSManagedObject *obj = (NSManagedObject *)[entityArray objectAtIndex:indexPath.row];
[managedObjectContext deleteObject:obj];
NSError *error;
[managedObjectContext save:&error];
// Delete the row from the data source
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:YES];
[self viewWillAppear:YES];
}
//[tableView endUpdates];
}
Any input on this problem would be appreciated.
Thanks.
Found same issue and stumbled upon this thread. But the reason for the tableview freeze issue was different in our case.
For the sake of posterity:
The UITableViewCell which goes into Edit mode to display the "insert" or "delete" buttons should never have its userInteractionEnabled property set to "NO".
By correcting this, the same tableview freezing issue was fixed for us.
I can't see a call to [tableView endUpdates] matching the [tableView beginUpdates] that is at start of the if.
Could it be for this reason that your table freezes?
I'm brand new to iPhone development (and first question posted here) and am sort of stuck with Core Data and Table Views.
In short, my app is crashing when I delete a row from my UITableView due to NSFetchedResultsChangeUpdate being called on a record that has already been removed due to a cascade delete on a self referring table.
Here is a description of the data model:
There are two Entities; Person and Connection.
Person contains name (String), connections (To Many Relationship to Connection->source, cascade delete rule) and connectedby (To Many Relationship to Connection->connection, cascade delete rule)
Connection contains relationship (String), source (Relationship to Person->connections, nullify delete rule) and connection (Relationship to Person->connectedby, nullify delete rule)
The idea being that there are two people connected by a relationship (eg Mother or Son)
In my TableViewController I implement the following:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
and
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
Person *person = nil;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
person = (Person *)[fetchedResultsController objectAtIndexPath:indexPath];
[self configureCell:(PersonTableViewCell *)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
person = (Person *)[fetchedResultsController objectAtIndexPath:indexPath];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
and
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
Here are the sample records I created for testing this:
Person *person1 = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.managedObjectContext];
[person1 setName:[NSString stringWithFormat: #"Tommy"]];
Person *person2 = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:self.managedObjectContext];
[person2 setName:[NSString stringWithFormat: #"Jane"]];
Connection *connection = [NSEntityDescription insertNewObjectForEntityForName:#"Connection" inManagedObjectContext:managedObjectContext];
[connection setConnection:person2];
[connection setSource:person1];
[connection setRelationship:[NSString stringWithFormat:#"Mother"]];
Connection *connection2 = [NSEntityDescription insertNewObjectForEntityForName:#"Connection" inManagedObjectContext:managedObjectContext];
[connection2 setConnection:person1];
[connection2 setSource:person2];
[connection2 setRelationship:[NSString stringWithFormat:#"Son"]];
When I delete the record at indexPath[0,0] i.e. Jane in this example since the view is sorted by name, I generate the following error:
2010-10-19 16:09:01.461 HelpMe[6324:207]
Serious application error.
Exception was caught during Core Data change processing.
This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification.
*** -[NSMutableArray objectAtIndex:]: index 1 beyond bounds [0 .. 0] with userInfo (null)
Detected an attempt to call a symbol in system libraries that is not present on the iPhone:
_Unwind_Resume called from function -[NSManagedObjectContext(_NSInternalChangeProcessing) _processRecentChanges:] in image CoreData.
The delete seems to correctly generate a NSFetchedResultsChangeDelete for indexPath [0,0] but also then immediately generates a NSFetchedResultsChangeUpdate for [0,1] which no longer exists since [0,1] is seemingly now in [0,0] after the delete.
Without the associated Connection record, it deletes fine.
I can seemingly work around this by simply calling [self.tableView reloadData] on controllerDidChangeContent instead of implementing begin/end updates and didChangeOnject: but I do not believe this is the proper way to handle this.
I appreciate any help anyone can offer.
I resolved a similar issue this by providing a non-nil sectionNameKeyPath to NSFetchedResultsController (in initWithFetchRequest).
I then avoided sections by using:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return #"";
}
Very annoying.
FYI, my issue was this error:
Serious application error. An
exception was caught from the delegate
of NSFetchedResultsController during a
call to -controllerDidChangeContent:.
* -[NSMutableArray objectAtIndex:]: index 0 beyond bounds for empty array
with userInfo (null)
At the top of - (void)controller:(NSFetchedResultsController *)controller didChangeObject: I was fetching the entity out of the fetchedResultsController. Well, that really messes up things when you have just deleted the object and need to update the table. I moved the entity fetch into the NSFetchedResultsChangeUpdate portion. This then kept me from crashing out.