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;
}
}
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;
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.
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With