Michael Ochs
Michael Ochs

Reputation: 2870

NSFetchedResultsController - Incomplete UI update in TableView

I am using an NSFetchedResultsController to refresh the data of a table view. The data itself is provided via an XML parser that runs on the background. After the parser finished, it saves the data into its own context. The NSFetchedResultsController picks up these changes immediately and starts calling the -(void)controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: delegate method for each updated element. This also is fast and looks totally normal in the log files.

However, in -(void)controllerDidChangeContent: I call UITableView's -(void)endUpdates. Then I see the update animation on the screen, but in all cells, beside the last one which is only half visible, the only thing that is visible is an image on the left side of the cell. All text labels are not visible. It takes about 5 to 10 seconds, then all the labels pop visible.

However if I ignore all the delegate calls of the NSFetchedResultsController and simply call [self.tableView reloadData] on -(void)controllerDidChangeContent: everything works without problems. The content is there immediately.

Has anybody an idea what I am doing wrong here? The profiler shows that the main thread is basically doing nothing. Touch events are handled properly, besides the events that are dispatched to the table view. These aren't handled. It seems like the table view is busy doing some serious work, but I really don't know what that could be, as the animation is already done.

Here is my implementation of the NSFetchedResultsControllerDelegate:

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath*)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath*)newIndexPath {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    UITableView* tableView = self.tableView;
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

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

        case NSFetchedResultsChangeUpdate:
    [(NewsItemCell*)[tableView cellForRowAtIndexPath:indexPath] updateWithNews:[self.fetchedResultsController objectAtIndexPath:indexPath]];
            break;

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

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    [self.tableView endUpdates];
}

And this is the code of my cell layout:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.fetchedResultsController.sections.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id<NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController.sections objectAtIndex:section];
    return sectionInfo.numberOfObjects;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    News* model = [self.fetchedResultsController objectAtIndexPath:indexPath];

    NewsItemCell* cell = (NewsItemCell*)[tableView dequeueReusableCellWithIdentifier:NewsCellReuseIdentifier];
    [cell updateWithNews:model];
    cell.accessoryType = (model.content ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone);
    return cell;
}

And the pretty basic update of the cell:

- (void)updateWithNews:(News*)news {
    NSString* dateString = [[NSDateFormatter outputDateFormatter] stringFromDate:news.date];
    self.headlineLabel.text = (news.headline ? news.headline : NSLocalizedString(@"<NewsNoHeadlineReplacement>", nil));
    self.metaInfoLabel.text = [NSString stringWithFormat:NSLocalizedString(@"<NewsMetaInfoFormatDate>", nil), (dateString ? dateString : (NSLocalizedString(@"<NewsNoDateReplacement>", nil)))];

    self.readIndicatorView.hidden = (news.read != nil && [news.read compare:news.parsingDate] == NSOrderedDescending);
}

The placeholder strings aren't shown either. The labels are completely empty. Only the image is visible!

Upvotes: 6

Views: 2409

Answers (3)

mdomans
mdomans

Reputation: 1303

Let's focus on this piece of code:

 case NSFetchedResultsChangeUpdate:
        [(NewsItemCell*)[tableView cellForRowAtIndexPath:indexPath] updateWithNews:[self.fetchedResultsController objectAtIndexPath:indexPath]];
        break;

To walk you through what's happening:

  1. you open updates transaction on tableView
  2. then you react to delegate calls by issuing commands to tableView
  3. but for updates you do it differently by issuing commands not related to tableView
  4. then you close transaction on tableView

What I mean is: tableView commands may bundle the changes into one transaction. Seems like your calls to rerender cells are ending up on the next transaction. So replace the code above with:

case NSFetchedResultsChangeUpdate:
        [tableView reloadRowsAtIndexPaths: [NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic ];
        break;

Upvotes: 6

LocoMike
LocoMike

Reputation: 5656

Are your calls to your NSFetchedResultsControllerDelegate in a background thread? That might be the issue. To test, you could merge the changes of your bg moc into the main moc and observe the main moc instead of the bg moc. An instance of a NSManagedObjectContext is only supposed to be used by one thread.

Upvotes: 2

Tomasz Zabłocki
Tomasz Zabłocki

Reputation: 1326

Things I would check first:

  1. Make sure your fetch is not executed in a different thread
  2. Make sure your fetch is quick, if it's too slow it might be the cause as well

Upvotes: 4

Related Questions