A few users of my app are getting a crash with my TableView which uses a NSFetchedResultsController to get the data source from Core Data. My View controller is a subclass of CoreDataTableViewController which contains the standard code for a NSFetchedResultsController in a UITableViewController.
The crash report says that the crash happened in tableView:commitEditingStyle:forRowAtIndexPath:] at the line (code is just below) for this reason:
Fatal Exception: NSInvalidArgumentException
-deleteObject: requires a non-nil argument
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.managedObjectContext deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
}
}
I tried to look for a similar problem but I didn't find anything and it doesn't always happen (never experienced during testing).
Here is the didChangeObject in my CoreDataTableViewController class:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
}
Can anyone help me?
Here's something you won't see documented, but it's nevertheless sometimes true: This message can in rare circumstances be fired twice. If it is, the second time indexPath will be nil.
The appropriate way to handle this is to check for nil first:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
if (indexPath == nil) return;
[self.managedObjectContext deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
}
}
I have had similar crashes in my delete routines for many months (possibly years?) and could never figure this out. Until one day, I got lucky and it happened while I was deleting rows with the debugger attached. I was able to see that indexPath was unexpectedly nil, and my array of items was already empty.
This means either the event was fired twice in a row, or the OS called it a second time in response to some action by the user after they had deleted the last item. I haven't been able to find out which, and I haven't seen it happen again. However, it explains a number of crash reports I've seen.
Related
If I click on a cell and a background Core Data refresh happens, my cell gets deselected.
After investigation, my NSFetchedResultsControllerDelegate's implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: gets called with a type of NSFetchedResultsChangeMove but indexPath and newIndexPath are identical (i.e. same row/section values).
The NSFetchedResultsChangeUpdate type doesn't get called.
My implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: is the typical one provided in Apple's documentation. I believe that switching from:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
...
switch(type) {
...
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
to:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
...
switch(type) {
...
case NSFetchedResultsChangeMove:
if (indexPath.section != newIndexPath.section || indexPath.row != newIndexPath.row) {
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
}
would solve my problem.
My question is: is this fix correct or do I miss something that could explain why my update is of the NSFetchedResultsChangeMove type?
My problem was that I was that I was assigning values to NSManagedObject instances that were the same as before. i.e., if foo.bar was 2, I would call the API and call a new foo.bar = 2 assignment on my object. Even though, values were identical, Core Data was still considering the value had changed.
It is still weird that I got a NSFetchedResultsChangeMove type instead of a NSFetchedResultsChangeUpdate type but heh.
What I did to resolve this is:
if ([object.changedValues count] == 0) {
// Reverting changed on object
[context refreshObject:object mergeChanges:NO]
}
At the end of each update, rather than comparing attributes one by one.
In my app, I manage a library of music files using CoreData as the backing store. Works pretty well. I also use an NSFetchedResultsController to bind the data to a table view for display, which is also all fine and dandy. The issue arises when I try to enable editing on the table view.
As usual, I return YES from tableView:canEditRowAtIndexPath: to allow rows to be editied, enabling the swipe to delete UI. That works nicely and shows up right. However, after deleting files and whatnot, the UITableView begins having some strange rendering issues:
I don't send any messages to the tableview to explicitly request UI updates, since the fetched results controller notifies my UITableViewController subclass of this, which then sends the appropriate messages:
/**
* Handler for changing an entire section at once
*/
- (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:UITableViewRowAnimationLeft];
break;
}
}
/**
* Handler for changing a single object, usually a row.
*/
- (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:UITableViewRowAnimationRight];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationLeft];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:#[newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
/**
* Called when the fetched results controller begins sending update messages.
*/
- (void) controllerWillChangeContent:(NSFetchedResultsController *) controller {
[self.tableView beginUpdates];
}
/**
* Called when the fetched results controller finishes sending updates.
*/
- (void) controllerDidChangeContent:(NSFetchedResultsController *) controller {
[self.tableView endUpdates];
}
After reading the docs, I'm pretty sure all I need to do is delete the object from the context, which happens like this:
- (void) tableView:(UITableView *) tableView commitEditingStyle:(UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:(NSIndexPath *) indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// perform on the object context's background queue
[[SQUPersistence sharedInstance].managedObjectContext performBlock:^{
[[SQUPersistence sharedInstance].managedObjectContext deleteObject:[_fetchedResultsController objectAtIndexPath:indexPath]];
// Save database
[[SQUPersistence sharedInstance] save:nil];
}];
}
}
Eventually, the UI enters a strange state as seen above and the table gets out of sync with the backing store, and no animations are performed. Am I doing this entire deletion thing wrong, or have I encountered a bug with Apple's classes?
I suspect it is threading issue. If you're deleting on the background queue, but referencing the object from the main queue, [_fetchedResultsController objectAtIndexPath:indexPath], I am sure that will cause problems.
Right off, I would confirm by trying:
- (void) tableView:(UITableView *) tableView commitEditingStyle:(UITableViewCellEditingStyle) editingStyle forRowAtIndexPath:(NSIndexPath *) indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = _fetchedResultsController.managedObjectContext;
[context deleteObject:[_fetchedResultsController objectAtIndexPath:indexPath]];
// Save database (You do not have to do this here)
[context save:nil];
}
}
If you do not have issues doing it this way, I'd suggest leaving it. There is likely little benefit in deleting a single object in the background. Where you might hang would be the save operation, which you don't actually need to do here. You can leave the save till user exits or some other point at which any lag would be masked by the UI change.
I have a table view with a search bar/display controller (all built in story board).
My problem is with deleting rows. The functionality of delete row works BUT the animation of row deletion seems to be unstable. Sometimes the deletion is animated and most of the times the row is removed immediately with no animation.
This is my delete method:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
if (tableView == self.searchDisplayController.searchResultsTableView) {
[_managedObjectContext deleteObject:[_notesFilteredFetcher objectAtIndexPath:indexPath]];
} else {
[_managedObjectContext deleteObject:[_notesFetcher objectAtIndexPath:indexPath]];
}
NSError *error;
if (![_managedObjectContext save:&error]) {
DLog(#"Failed deleting note : %#", [error localizedDescription]);
}
}
}
The table view is using core data so the deletion of the rows occur in the appropriate controller's method as follows:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = (controller == _notesFilteredFetcher) ? self.searchDisplayController.searchResultsTableView : self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath forTable:tableView];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray
arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
I also noticed that when animation for delete does occur, all rows above the deleted row move a bit down and the rows under the deleted row move up until the empty space is covered.
If I look at the built-in mail app and delete some rows (i.e. mails) it does seem to animate as expected every time and ONLY the rows below the deleted row are moved up to cover the empty space (the rows above the deleted row do not move).
Any ideas ?
UPDATE:
I have noticed that animation does occur only for the last row. If I delete any other row no animation occurs but if I delete the last row animation does work.
If someone bumps into the same problem :
I noticed that I called reloadData method of the table view right after the call to endUpdates of the same table view as I needed to update the cell's internal indices.
This will cause the animation to stop immediately. Removing the reloadData method solved the animation issue.
I have a UITableView backed by an NSFetchedResultsController that shows items which have been bookmarked by the user. Items can be un-bookmarked from a button within the row, which leads to the problem. After an item is un-bookmarked, it should disappear from the table view because it no longer matches the predicate, but because my row counts per section have been altered by the update, I get a variation of this error:
CoreData: error: Serious application error. An exception was caught from the delegate 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 (4), 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)
Here's my very simple didChangeObject method:
-(void)controller:(NSFetchedResultsController *)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
[super controller:controller didChangeObject:anObject atIndexPath:indexPath forChangeType:type newIndexPath:newIndexPath];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
Is there some way that I can instruct the NSFetchedResultsController not to sweat the mismatched counts? Or do I need a different approach entirely?
Your didChangeObject delegate method looks very incomplete,
in particular it does not check which event occurred (insertion, deletion or update).
You can find a template in the NSFetchedResultsControllerDelegate protocol
documenation.
The method looks normally similar to this:
- (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:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
You should also implement the controller:didChangeSection:atIndex:forChangeType:
delegate method.
And I do not understand what the
[super controller:controller didChangeObject:anObject atIndexPath:indexPath forChangeType:type newIndexPath:newIndexPath];
call is for!
Martin R gave the good answer, but he omit one important thing:
If the FetchControllerProtocol wants to refresh a lot of rows at the same time, it would probably crash.
The Apple Doc give a very clear typical example of the process. It's important to surround your table changes by beginUpdates & endUpdates methods.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView endUpdates];
}
you should also implement the section refresh if you have sections
- (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;
}
}
Hope this helps
Even though I have worked with NSFetchedResultsController dozens of times, I have run into a problem I can't seem to find the cause of.
I have a UIViewController with a NSFetchedResultsController that manages the data source of a table view. Whenever I delete an object in the NSFetchedResultsController, the table view is updates by the fetched results controller, but when I save the NSManagedObjectContext (to commit the delete), an insert is triggered for the object that was just deleted. The net result is that the object is not removed from the table view.
The odd thing is that when the application is restarted, the object that was supposed to be deleted is actually deleted from the store and doesn't show up in the table view.
To be clear, I use only one managed object context, which is only accessed on the main thread. In other words, the setup is quite simple.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Fetch Store
CCDStore *store = [self.fetchedResultsController objectAtIndexPath:indexPath];
// Delete Store
[self.fetchedResultsController.managedObjectContext deleteObject:store];
// Save Changes
NSError *error = nil;
[self.fetchedResultsController.managedObjectContext save:&error];
if (error) {
NSLog(#"Error %# with user info %#.", error, error.userInfo);
}
}
}
Even though the deleted object is inserted in the fetched results controller, the object is turned into a fault, but I don't understand why it is added to the fetched results controller in the first place.
No exceptions or errors are thrown. I have performed several checks such as calling isDeleted and validateForDelete on the object to find the cause, but I have not become any wiser.
EDIT:
I am pretty sure that the fetched results controller's delegate methods are not the culprit, but for completeness I have added the implementation of controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
[self updateView];
switch(type) {
case NSFetchedResultsChangeInsert: {
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeDelete: {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
case NSFetchedResultsChangeUpdate: {
[self configureCell:(CCDStoreCell *)[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
}
case NSFetchedResultsChangeMove: {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
default: {
break;
}
}
}