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;
}
}
}
Related
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.
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.
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 simple table view with 1 section and 2 rows. I'm using a NSFetchedResultsController to keep the table sync'd with CoreData. I make a change to one of the rows in CD which triggers a table view cell to be updated and moved. The problem is that when cellForRowAtIndexPath: gets called during the NSFetchedResultsChangeUpdate, the wrong cell is returned (this makes sense b/c the cells haven't been moved yet). So the wrong cell is updated with with the newly updated data. After that the NSFetchedResultsChangeMove message is handled so the cells trade places (neither cell's content is updated since its just a move call). The result is both cells reflect the data from the newly updated CD entity. Reloading the table fixes the issue. I'm running iOS 6.
In other words if the cell at index 0 represents entity A and index 1 represents entity B and I update entity A to A' in such a way that the 2 cells reverse order, the result is that I see 0:A' 1:A when I would expect 0:B, 1:A'.
- (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:
//the wrong cell is updated here
[self configureCell:(SyncCell*)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
//this code produces errors too
//[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
//[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
A solution is:
[self configureCell:(SyncCell*)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:newIndexPath ? newIndexPath : indexPath];
Using the new index path when its supplied during the update. And then use delete and insert instead of move. I'd still like to know if anyone else has any input.
I would recommend you to take a look at this blogpost
Fixing the bug is easy. Just rely on the behavior of UITableView I described above and replace the call to configureCell:atIndexPath: with the reloadRowsAtIndexPaths:withRowAnimation: method, which will automatically do the right thing:
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
When invoking configureCell, look up the indexPath based on the object passed in. Both indexPath and newIndexPath are unreliable at this point. For example:
case NSFetchedResultsChangeUpdate:
myPath = [controller indexPathForObject:anObject];
if (myPath) {
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:myPath];
}
break;
The only solution that worked for me: Thomas Worrall's Blog: Reordering rows in a UITableView with Core Data
First of all, Its necessary create an attribute in your NSManagedObject to hang the last order of your object.
#interface MyEntity : NSManagedObject
#property (nonatomic, retain) NSNumber * lastOrder;
#end
Then, declare a property in your ViewController.m:
#interface ViewController ()
#property (nonatomic) BOOL isReordering;
#end
In method tableView:moveRowAtIndexPath:toIndexPath:, manage the changes in a temporary array:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
_isReordering = YES;
NSMutableArray *arrayNewOrder = [[_fetchedResultsController fetchedObjects] mutableCopy];
MyEntity *myManagedObject = [arrayNewOrder objectAtIndex:fromIndexPath.row];
[arrayNewOrder removeObjectAtIndex:fromIndexPath.row];
[arrayNewOrder insertObject:myManagedObject atIndex:toIndexPath.row];
for (int i=0; i < [arrayNewOrder count]; i++)
{
myManagedObject = [arrayNewOrder objectAtIndex:i];
myManagedObject.lastOrder = [NSNumber numberWithInt:i];
}
_isReordering = NO;
NSError *error;
BOOL success = [self.fetchController performFetch:&error];
if (!success)
{
// Handle error
}
success = [[self managedObjectContext] save:&error];
if (!success)
{
// Handle error
}
}
Thomas explained about perform fetch and save context:
I'm not entirely sure why the fetch needs to be performed first, but
it fixed a ton of crazy bugs when I did it!
The last trick is handling NSFetchedResultsController delegate method controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:, specifically in NSFetchedResultsChangeMove:
case NSFetchedResultsChangeMove:
{
if (!_isReordering)
{
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
}
break;
}
It worked for me! I hope It helps you!
i use NSFetchedResultsController to populate tableview and every thing is ok(adding, deletng..).. But whenever i try to remove last row from tableview(when there is only one row in tablview) the app crashes with the log below:
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-1914.84/UITableView.m:1037
2012-08-03 16:32:39.667 MackaWithCoreData[793:15503] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (1) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out). with userInfo (null)
As far as i see, after deleting the object when the delegate methods try to reload the tableview since the fetchedResultsController is null it causes a crash.. How to handle this issue? Thanks in advance
EDIT Implementation of controller controllerDidChangeContent:... method is below
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath
{
switch(type)
{
case NSFetchedResultsChangeInsert:
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
XLog(#"the row removed from tv");
break;
case NSFetchedResultsChangeUpdate:
// [self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
XLog(#" object deleted from fetchedresultscontroller");
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
After a few days i decided a different logic to solve this issue. I firstly control the count of fetchedObjects on fetchedResultsController, if the count is greater than 1 i call deletion & saving delegates on database, but if the count is equal to 1, just push to a different viewController. On that vc too, i'll disable navigation controller back to this view controller untill a new message comes. And when a new message comes, i will firstly delete the object on entity and than add the message..
Here is some pseudo code & algorithm:
if ([[self.fetchedResultsController fetchedObjects]count] >1) {
callDeletionDelegateForDatabase;
}else{
declare a viewController object;
pushToThatVC;
}
// on the class of target vc
hide_back_navigation_back_to_theViewControllerYouCameFrom;
//when a new message comes from web service
reset Entitiy(the property we need its value)
show_navigation_Controller_To_theViewControllerOfMessages...
I posted this stupidly coded pseudo to help others who faces the same situation, if anyone knows a better approach, please write down.
Below is my code:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
if ([[self.fetchedResultsController fetchedObjects]count] >1) {
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
[context deleteObject:[self.fetchedResultsController objectAtIndexPath:indexPath]];
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}else {
BNT_DetailViewController *vc=[BNT_DetailViewController new];
[self.navigationController pushViewController:vc animated:YES];
XLog(#"You Cant delete this");
}
}
}