UITableView dynamic cell sizing and asynchronously loading images resulting in weird scrolling behavior

189 Views Asked by At

I have a UITableView + NSFetchedResultsController combination for loading comments. The comments include name, date, body, and importantly an avatar image. The comments are stored in Core Data.

Upon viewDidLoad, it checks to see if the comments have been loaded from my source. If they have not been downloaded it spawns an NSOperation to download and scrape the HTML to get the name, date, body, and URL for the avatars for each comment. It also makes the Core Data entities for all of these. Being a background threaded NSOperation, it uses a private NSManagedObjectContext to do the work and saves it back into the main MOC.

Once those are added to the store, my NSFetchedResultsController calls the delegate methods controllerWillChangeContent:, didChangeSection:, didChangeObject:, and controllerDidChangeContent:. It works fine when inserting the batch of new comments, and inserts them into the table view just fine.

My table view cells are using iOS8's UITableViewAutomaticDimension to have dynamic heights, depending on the length of content for each comment. If I only show the text of each comment (ignoring avatar images for now) everything works as expected and scrolling through comments works fine, and it all looks ordinary.

However my problem comes in when it's loading avatar images. In my configureCell: (which is called by cellForRowAtIndexPath: and controller:didChangeObject:) it checks to see if the image has been downloaded yet or not. If not, it spawns a new, different kind of NSOperation to go fetch the image based on the comment's avatarImageURL property. When it has been fetched, it adds the data of the image into the Core Data entity. It also signals the table view controller via a custom delegation method that the image has been updated. My fetched results controller also signals my table view controller view via controller:didChangeObject:.

This is where the problems come in.


When scrolling very fast, and for the first time this data is being downloaded and stored into Core Data the table view and the dynamically resizing cells act very weird. they change size many times when scrolling both up and down. And only after having scrolled through the entirety of the cells once or twice, everything is smooth after that.

When i completely disable the spawning of NSOperations to download the avatars, everything again works and scrolls fine. However, if I let it download the images and add them to the Core Data entity, but fail to implement the custom delegation method and the NSFetchedResultsChangeUpdate case for controller:didChangeObject:, it still results in the weird, choppy scrolling initially.

One thing I did notice, via NSLogs, is that all comments are inserted into the table view in a single batch, whereas every time a single avatar is downloaded, the fetched results controller will issue willChangeContent:, didChangeObject:, and didChangeContent:. Again, those three methods all for a single avatar.

Relevant methods (I can add more if necessary):

- (void)viewDidLoad {
    [super viewDidLoad];

    // First get our current array of comic infos, initiating the controller
    NSError *fetchError;
    if (![[self fetchedResultsController] performFetch:&fetchError]) {
        NSLog(@"Error in the fetched results controller: %@", fetchError.description);
        return; // Don't do anything else.
    }

    self.tableView.estimatedRowHeight = 70.0f;
    self.tableView.rowHeight = UITableViewAutomaticDimension;

    if (self.comicInfo.comments == nil || self.comicInfo.comments.count == 0) {
        CommentsRetrieverOperation *operation = [[CommentsRetrieverOperation alloc] initWithComicInfoID:self.comicInfo.objectID];
        operation.managedObjectContext = self.managedObjectContext;
        [self.commentDownloadQueue addOperation:operation];
    }
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CommentCell" forIndexPath:indexPath];

    // Configure the cell...
    [self configureCell:cell atIndexPath:indexPath];

    return cell;
}

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    CommentTableViewCell *commentCell = (CommentTableViewCell *)cell;

    Comment *comment = [self.fetchedResultsController objectAtIndexPath:indexPath];

    commentCell.nameLabel.text = comment.name;
    commentCell.dateLabel.text = comment.date;
    commentCell.commentLabel.text = comment.content;
    commentCell.commentLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];

    if ([comment isAdmin].boolValue == true) {
        commentCell.contentView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"BackgroundPattern1"]];
    } else if (indexPath.row % 2 == 0) {
        commentCell.contentView.backgroundColor = [UIColor colorWithRed:248.0f/255.0f green:248.0f/255.0f blue:248.0f/255.0f alpha:1.0f];
    } else {
        commentCell.contentView.backgroundColor = [UIColor whiteColor];
    }

    if (comment.avatarImage == nil || comment.avatarImage.image == nil) {
        CommentImageDownloadOperation *operation = [[CommentImageDownloadOperation alloc] initWithCommentID:comment.objectID];
        operation.managedObjectContext = self.managedObjectContext;
        operation.delegate = self;
        operation.userInfo = @{ @"cell":commentCell, @"indexPath":indexPath };
        [self.avatarDownloadQueue addOperation:operation];
    } else {
        [self setAvatarImageForCell:commentCell withComment:comment];
    }

    commentCell.userInteractionEnabled = NO;
}

- (void)setAvatarImageForCell:(CommentTableViewCell *)cell withComment:(Comment *)comment {
    if (comment.avatarImage.isDefaultImage.boolValue) {
        cell.avatarImage = self.defaultAvatarImage;
    } else {
        UIImage *image = [UIImage imageWithData:comment.avatarImage.image];
        cell.avatarImage = image;
    }
}

- (void)commentImageDownloadOperation:(CommentImageDownloadOperation *)operation didFinishDownloadWithUserInfo:(id)userInfo {
    NSIndexPath *indexPath = [self.tableView indexPathForCell:userInfo[@"cell"]];
    if (indexPath == userInfo[@"indexPath"]) {
        CommentTableViewCell *cell = userInfo[@"cell"];
        Comment *tempComment = (Comment *)[self.managedObjectContext objectWithID:operation.commentID];
        [self setAvatarImageForCell:cell withComment:tempComment];
    }
}

#pragma mark - Fetched Results Controller Delegate
All of these methods are copied exactly from Apple's sample at https://developer.apple.com/library/ios/documentation/CoreData/Reference/NSFetchedResultsControllerDelegate_Protocol/index.html#//apple_ref/doc/uid/TP40008228-CH1-SW10

So put shortly, my problem is that when loading images in the background for dynamically sized cells, and with using Core Data and a fetched results controller, the scrolling is very choppy. Is there a better way to implement this all, and am I implementing this wrong? Is there a better way than just loading all images at the very beginning?

Edit: Some clarification. As I scroll down in the table view, the fetched results controller sends updates to the objects. So when I scroll down willChangeContent:, didChangeObject: for type=NSFetchedResultsChangeUpdate, and didChangeContent: are called for each new cell entering the view.

0

There are 0 best solutions below