edukulele
edukulele

Reputation: 489

Infinite loop in NSFetchedResultsControllers controllerDidChangeContent method

I have UITableViewController table and fetched rows from Core Data using NSFetchedResultsController. My tableView is responsible for layering data. Eatch row position represents layer (at zero row highest layer, n - row lowest). Lets say we have object with name Layers *layers. When I create new layer, it needs to go to 1st row (indexPath.row == 0) in the table and needs to get layers.layer.position = 0. When I create new layer or some other changes are made (move, delete) I need to refresh Core data object layers (each layer needs to get new position value). But if I refresh layers position by calling [self updateLayersPosition] method iside of controllerDidChangeContent method it goes to infinite loop because everytime on every old layer value change it returns to the same controllerDidChangeContent method again and again. So the question is how to "bind" table row or fetched controllers indexPath.row with layers.layer.position? Maybe I can some how stop fetchedresultscontroller from fetching, do updates and then start fetching again? Or maybe there is some other way to do it?

Here is the code:

-(void)updateLayers{
    for (int i = 0; i < [self.tableView numberOfRowsInSection:0]; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i 
                                                    inSection:0];
            ((Layer *)[self.fetchedResultsController objectAtIndexPath:indexPath]).layer.position = i;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self updateLayers];
    [self.tableView endUpdates];
}

Upvotes: 2

Views: 625

Answers (1)

Michał Ciuba
Michał Ciuba

Reputation: 7944

All the willChange / didChange methods in NSFetchedResultsControllerDelegate are called after you modify Core Data objects. So if you modify any NSManagedObject inside controllerDidChangeContent:, it will trigger controllerDidChangeContent: again, leading to the infinite loop.

You can think about these delegate callbacks as a controller saying "You have changed something in your Core Data model, now let's update the UI (the UITableView). The general pattern you should follow is:

  • make one or more changes (additions, modifications, deletions) in NSManagedObjects
  • save NSManagedObjectContext
  • in the methods of NSFetchedResultsControllerDelegate: update the UITableView to reflect the changes (add, reload, delete table view rows)

So you probably want to do something like that:

//in the method in which you add a new Layer object
- (void)addLayer {
  Layer *layer = [NSEntityDescription insertNewObjectForEntityForName:@"Layer" inManagedObjectContext:context];
  //recalculate the position of all the other layers
  //save NSManagedObjectContext
}

The key is to update the position of each layer immediately after adding new layer, and then save the context. Then NSFetchedResultsController will take care of "binding" the layer's position to the row in the table, as you wanted (I assume that you use the position property in a sort descriptor used in a fetch request). If you implement these NSFetchedResultsControllerDelegate methods as proposed in documentation, it should do the job:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}

- (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:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath]
                  atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
                       withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}


- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

Upvotes: 2

Related Questions