Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simultaneous move and update of UITableViewCell and NSFetchedResultsController

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;
    }
}
like image 262
Tylerc230 Avatar asked Jan 16 '13 08:01

Tylerc230


4 Answers

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;
like image 26
Toydor Avatar answered Oct 30 '22 13:10

Toydor


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.

like image 64
Tylerc230 Avatar answered Oct 30 '22 14:10

Tylerc230


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;
like image 40
Bob Glass Avatar answered Oct 30 '22 13:10

Bob Glass


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!

like image 26
Thomás Pereira Avatar answered Oct 30 '22 15:10

Thomás Pereira