Search code examples
iosparse-platformpfquery

Retrieve number of comments


I'm trying to retrieve the total number of comments from a PFQuery. For some reason, the log shows the array being returned but the label doesn't change with the number as required. Here's the code:

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"FeedCell";

FeedCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
    cell = [[FeedCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
    PFObject *post = [postArray objectAtIndex:indexPath.row];



[cell.captionView setText:[post objectForKey:@"tag"]];

cell.captionView.editable = NO;

cell.captionView.text = [post objectForKey:@"description"];




PFFile *theImage = [post objectForKey:@"image"];
NSData *imageData = [theImage getData];

 cell.photoImageView.image = [UIImage imageWithData:imageData];

cell.selectionStyle = UITableViewCellSelectionStyleNone;

cell.captionView.selectable = NO;


[cell.shareThis setTintColor:[UIColor clearColor]];

cell.comments.tag = indexPath.row;
cell.likeForYa.tag = indexPath.row;


[cell.likeLabel setText:@""];
PFQuery *commentsQuery = [PFQuery queryWithClassName:@"Comment"];


[commentsQuery whereKey:@"photo" equalTo:post.objectId];

NSLog(@"sement: %@", commentsQuery);

[commentsQuery countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
    if (number == 1) {
        cell.likeLabel.text = @"1 comment";
        NSLog(@"comment: %d", number);}
    else if (number > 0) {
        [cell.likeLabel setText:[NSString stringWithFormat:@"%d comments", number]];
        NSLog(@" plus: %d", number);

    }
}];

    return cell;


  }

The portion of the code to be doing the query is

PFQuery *commentsQuery = [PFQuery queryWithClassName:@"Comment"];
[commentsQuery whereKey:@"photo" equalTo:post.objectId];

NSLog(@"sement: %@", commentsQuery);

[commentsQuery countObjectsInBackgroundWithBlock:^(int number, NSError *error) {
    if (number == 1) {
        cell.likeLabel.text = @"1 comment";
        NSLog(@"comment: %a", number);}
    else if (number > 0) {
        [cell.likeLabel setText:[NSString stringWithFormat:@"%d comments", number]];

    }
}];

Could someone please help me out? Thank you!


Solution

  • The table view cell needs a fact (a count) that is received asynchronously. It's natural to attempt that asynch request in cellForRowAtIndexPath, but it isn't good practice: (a) that request to will be fired over and over when the user scrolls, and (b) the cell that needs the fact may be reused (may correspond to a different row) by the time the request completes. Here's a better pattern:

    Isolate the network code, just to stay sane:

    - (void)commentCountForPost:(PFObject *)post withCompletion:(void (^)(NSNumber *))completion {
        PFQuery *commentsQuery = [PFQuery queryWithClassName:@"Comment"];
        [commentsQuery whereKey:@"photo" equalTo:post];
    
        NSLog(@"sement: %@", commentsQuery);
    
        [commentsQuery findObjectsInBackgroundWithBlock:^(NSArray *array, NSError *error) {
            completion(@(array.count));  // wrap as an NSNumber
        }];
    }
    

    Cache the results, so that we request up to one time for each row:

    // keys will be indexPaths, values will be comment counts
    @property(nonatomic,strong) NSMutableDictionary *commentCounts;
    
    // be sure to initialize early to
    self.commentCounts = [@{} mutableCopy];
    

    Now in cellForRowAtIndexPath, remember a couple important things: (a) check the cache for an already fetched value, (b) do not retain the cell in the completion block, it may refer to the wrong row by the time the block runs. Instead, reload the row, knowing that the cached value will be there:

    // ...
    PFObject *post = postArray[indexPath.row];
    // ...
    [cell.likeLabel setText:@""];
    NSNumber *commentCount = self.commentCounts[indexPath];
    if (commentCount) {
        self.likeLabel.text = [NSString stringWithFormat:@"%@ comments", commentCount];
    } else {
        [self commentCountForPost:post withCompletion:^(NSNumber *count) {
            self.commentCounts[indexPath] = count;  // cache the result
            [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
        }];
    }
    return cell;
    

    Sometimes I add the cache logic to the network code. How to do that should be obvious, but I can demonstrate if you'd like.

    EDIT Hopefully you can see from the logic of the solution that when the server data changes, the client's cache goes out of date, and should be discarded. When this view controller knows about the change, it can do this:

    // imagine we know the comment count changed at a specific indexPath
    [self.commentCounts removeObjectAtIndex:indexPath.row];
    [self.tableView reloadRowsAtIndexPaths:@[indexPath]];
    
    // or, imagine we know that the comment count changed someplace, or in more than one places.  call this...
    - (void)emptyCacheAndReloadData {
        [self.commentCounts removeAllObjects];
        [self.tableView reloadData];
    }
    

    But if another view controller makes the change, this vc needs to learn about it, and that's a different problem of a sort often asked about on SO. I'd encourage you to read the answer given here, which is correct and fairly comprehensive. If this is the first time you've tackled that topic, you may -- understandably -- want to first try a little shortcut. That would be this (matching your intuition about viewWillAppear):

    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
        [self emptyCacheAndReloadData];
    }
    

    EDIT 2 The lazy load and cache approach described here expends the effort to do the asynch work as each table cell needs display. Once the cache is initialized for a row, display of that row is fast, but the table will feel a little bumpy on the first scroll through.

    We have to do the counting work someplace, and the best place is probably in the cloud, after saving a comment. There we could grab the post that the comment pertains to, count it's total comments, and save that sum on the post. With that you can skip my whole solution above, and just say something like...

    self.likeLabel.text = [NSString stringWithFormat:@"%@ comments", post[@"commentCount"]];
    

    But this assumes you're maintaining a comment count property on Post using cloud code. Without cloud code, we need to move the initial work someplace else on the client. It must happen after the posts (your postArray) are loaded, but before the table view is reloaded. Find that place in your code and call a function like this...

    - (void)postsLoaded {
        // build an array of indexPaths in your table.  this is just a row for each post
        NSMutableArray *indexPaths = [@[] mutableCopy];
        for (int i=0; i<self.postArray.count; i++) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
            [indexPaths addObject:indexPath];
        }
        // now preload the counts
        [self preloadCountsForIndexPaths:indexPaths completion:^(BOOL success) {
            [self.tableView reloadData];
        }];
    }
    
    // this is a recursive method.  to count comments on array of posts
    // count them on the first post, then count comments on the rest
    - (void)preloadCountsForIndexPaths:(NSArray *)indexPaths completion:(void (^)(BOOL))completion {
        if (indexPaths.count == 0) return completion(YES);
        NSIndexPath *indexPath = indexPaths[0];
        NSArray *remainingIndexPaths = [indexPaths subarrayWithRange:NSMakeRange(1, indexPaths.count-1)];
        PFObject *post = self.postArray[indexPath.row];
        [self commentCountForPost:post withCompletion:^(NSNumber *count) {
            self.commentCounts[indexPath] = count;  // cache the result
            [self preloadCountsForIndexPaths:remainingIndexPaths completion:completion];
        }];
    }