views:

60

answers:

2

I'm using an NSFetchedResultsController in a standard way to update a UITableView whenever something changes in the related core-data entities. I'm doing the same as described in the Apple documentation.

The problem I have is when I make a mass insert of new core-data entities. This causes the NSFetchedResultsController delegate to allocate (and insert) a new cell for each entity, but it does this without recycling the UITableViewCells (i.e., dequeueReusableCellWithIdentifier: always returns null). This means the allocation of potentially 100s of UITableViewCells, which can lead to memory problems. Does anyone know of a fix or workaround? Thanks.

Edit 1:

Within my UITableViewController subclass I have the standard NSFetchedResultsControllerDelegate methods. I believe this is identical to the example from Apple.

-(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:UITableViewRowAnimationTop];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationBottom];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationBottom];
            // Reloading the section inserts a new row and ensures that titles are updated appropriately.
            [tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationTop];
            break;
    }
}

-(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:UITableViewRowAnimationTop];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationBottom];
            break;
    }
}

-(void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // The fetch controller has sent all current change notifications, so tell the table view to process all updates.
    [self.tableView endUpdates];
}

Also within my UITableViewController subclass I have the following for fetching a cell for a given indexPath:

-(void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    Waypoint *waypoint = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = [waypoint comment];
}

-(UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {    
    static NSString *CellIdentifier = @"WaypointCell"; // matches identifier in XIB

    UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        NSLog(@"new cell");
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];      
        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    } else {
        NSLog(@"recycled cell");    
    }

    [self configureCell:cell atIndexPath:indexPath];

    return cell;
}

In the tableView:cellForRowAtIndexPath: function I added NSLog statements to display what's going on.

Here is what's happening. The above mentioned UITableViewController is pushed onto the navigation stack. An asynchronous request goes out and fetches a bunch of data, and creates or modifies the data related to this fetchController. Once [self.tableView endUpdates] gets called, the system begins creating and inserting the UITableViewCells into the UITableView. In the debugger console the output "new cell" is printed multiple times (can number in the 100s), which I believe is one for each new entity created. Only after the tableview is loaded (if it didn't crash due to memory problems) and I begin scrolling do I see the "recycled cell" output in the console.

+1  A: 

Are you sure you are using the exact same identifier both for creating and dequeueing the cell? (same identifier for dequeueReusableCellWithIdentifier: and initWithStyle:reuseIdentifier:)

Florin
I'm quite certain it's correct. I put NSLog statement at various places to monitor behaviour, and everything works correctly on normal table browsing (scrolling up and down). On table load I see an initial allocation of 5 or 6 cells, and as I scroll up and down they get reused. It's only with a mass table update (the original described problem) that I see 100s get allocated.Also, after the mass allocation, most get deallocated. I assume these are the non visible tablecells since scrolling then goes back to expected behaviour.
chris
A: 

I found a solution I'm happy with. The problem isn't NSFetchedResultsController per se, rather that it calls [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationTop] potentially hundreds of times from the NSFetchedResultsController delegate.

My solution is to introduce a massUpdate boolean that controls whether the NSFetchedResultsController should insert the new table rows as above, or whether it should do a simple [tableView reloadData] instead. I set this depending on the number of rows I plan to insert. Details of the implementation can be found at the following forum post:

https://devforums.apple.com/message/181219#181219

chris
While working on a new project, I found the following comment in Apple's RootViewController.m template file below the NSFetchedResultsControllerDelegate methods: "Implementing the above methods to update the table view in response to individual changes may have performance implications if a large number of changes are made simultaneously. If this proves to be an issue, you can instead just implement controllerDidChangeContent: which notifies the delegate that all section and object changes have been processed." It seems like this is a known problem.
chris