Search code examples
iosuitableviewnsfetchedresultscontroller

NSFetchedResultsController indexPathForObject not counting sections


I am updating a download view/button on a cell, and when I go to update my cell, I am not getting the correct section.

My code to get the index and update the download progress is this:

Object *obj = (Object *)notification.object;
NSIndexPath *index = [self.fetchedResultsController indexPathForObject:obj];
MyTableViewCell *cell = (MyTableViewCell *)[self.tableView cellForRowAtIndexPath:index];
DownloadProgressButtonView *buttonView = (DownloadProgressButtonView *)cell.accessoryView;
NSNumber *progressLong = [notification.userInfo objectForKey:@"progress"];
float progress = [progressLong floatValue];
NSNumber *totalBytesLong = [notification.userInfo objectForKey:@"totalBytes"];
float totalBytes = [totalBytesLong floatValue];
buttonView.progress = progress *.01;
float totalDownloadEstimate = totalBytes / 1.0e6;
float megaBytesDownloaded = (progress *.01) * totalDownloadEstimate;
cell.bottomLabel.text = [NSString stringWithFormat:@"%.1f MB of %.1f MB", megaBytesDownloaded, totalDownloadEstimate];

If I have two objects, each in a different section, they have the same row (0). When I go to update my cell, it updates the cell in section 1 instead of section 0. How do I fix this?

I can put whatever other code is needed. It works perfectly if I just disable sections in my NSFetchedResultsController.

My NSFetchedResultsController and delegates.

- (NSFetchedResultsController *)fetchedResultsController
{

    if (_fetchedResultsController != nil) {
        return _fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext];
    [fetchRequest setEntity:entity];

    NSSortDescriptor *nameString = [[NSSortDescriptor alloc] initWithKey:self.sectionSortDescriptor ascending:NO];
    NSSortDescriptor *descriptor = [[NSSortDescriptor alloc] initWithKey:self.sortDescriptor ascending:YES];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObjects:nameString,descriptor, nil]];
    NSString *downloadStartedString = @"Preparing to download";
    NSString *downloadingString = @"Downloading";
    NSString *downloadPausedString = @"Download paused";
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(downloaded == YES) OR (downloadStatus like[cd] %@) OR (downloadStatus like[cd] %@) OR (downloadStatus like[cd]%@)",downloadPausedString, downloadStartedString,downloadingString];
    [fetchRequest setFetchBatchSize:20];
    _fetchedResultsController =
    [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                        managedObjectContext:self.managedObjectContext sectionNameKeyPath:self.sectionNameString
                                                   cacheName:nil];
    _fetchedResultsController.delegate = self;
    self.fetchedResultsController = _fetchedResultsController;

    return _fetchedResultsController;
}

/*
 NSFetchedResultsController delegate methods to respond to additions, removals and so on.
 */
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {

    // The fetch controller is about to start sending change notifications, so prepare the table view for updates.
    [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:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeUpdate:
            [self configureCell:(StudioTableViewCell *)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;

        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            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:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeMove:
            NSLog(@"A table item was moved");
            break;
        case NSFetchedResultsChangeUpdate:
            NSLog(@"A table item was updated");
            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];
}

Finally when the download status changes, I update the object and send a notification to update the cell with the new status:

- (void)updateCell:(NSNotification *)notification
{
    Object *obj = (Object *)notification.object;
    NSIndexPath *index = [self.fetchedResultsController indexPathForObject:obj];
    [self.tableView reloadRowsAtIndexPaths:@[index] withRowAnimation:UITableViewRowAnimationFade];
}

Solution

  • Updating cells this way is not reliable. A cell updated in such a way will sooner or later be reused. The cell's subviews will be re-configured by tableView:cellForRowAtIndexPath:, based on the data provided by the datasource.

    You should make changes to the Object itself (instead of passing them in notification's userInfo) and save the managed object context. Then NSFetchedResultsControllerDelegate callbacks will fire, allowing you to reload the corresponding row. Then you should set all the properties of MyTableViewCell in configureCell:atIndexPath.

    And the configureCell: method should be called from cellForRowAtIndexPath method, not from the fetched results controller delegate method. The general pattern is to call reloadRowsAtIndexPaths: in controllerDidChangeObject:. Otherwise you can run into some cell reuse issues.

    An idea on how the code should look like:

    - (void)updateCell:(NSNotification *)notification
    {
        //depending on your Core Data contexts setup,
        // you may need embed the code below in performBlock: on object's context,
        // I omitted it for clarity
        Object *obj = (Object *)notification.object;
        //save changes to the object, for example:
        NSNumber *progressLong = [notification.userInfo objectForKey:@"progress"];
        obj.progress = progressLong; 
        //set all the properties you will need in configureCell:, then save context
        [obj.magagedObjectContext save:&someError];
    }
    

    then the fetched results controller will call controllerDidChangeObject:, in this method you should reload the row:

        case NSFetchedResultsChangeUpdate:
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
            break;
    

    finally, configure the cell (let's assume that you call configureCell:atIndexPath from tableView:cellForRowAtIndexPath:):

    - (void)configureCell:(MyTableViewCell*)cell atIndexPath:(NSIndexPath*)indexPath {
        Object *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    
        DownloadProgressButtonView *buttonView = (DownloadProgressButtonView*) cell.accessoryView;
        buttonView.progress = object.progress.floatValue *.01;
        //and so on
    }