Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 593
Michael Ochs Avatar asked Oct 14 '12 11:10

Michael Ochs


2 Answers

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;
like image 89
mdomans Avatar answered Sep 23 '22 18:09

mdomans


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
like image 33
Tomasz Zabłocki Avatar answered Sep 26 '22 18:09

Tomasz Zabłocki