0

I have a UIViewController with a tableView as a subview. The tableview will have 3 sections, and I used the answer to my question PFQueryTableViewController with 3 Sections to accomplish this, but the data is not getting loaded into the table. I am executing my query in the background in viewWillAppear. I fear that this is due to the nested query blocks. How can I fix this? Here is my code:

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


    PFQuery *gameQuery = [PFQuery queryWithClassName:@"Game"];
    [gameQuery whereKey:@"players" equalTo:[PFUser currentUser]];
    [gameQuery orderByDescending:@"createdAt"];

    [gameQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error){

        self.myTurn = [NSMutableArray array];
        self.theirTurn = [NSMutableArray array];
        self.gameOver = [NSMutableArray array];
        self.allGames = [NSArray array];

        for(PFObject *object in objects)
        {
            if([object objectForKey:@"isOver"] == [NSNumber numberWithBool:YES])
            {
                [self.gameOver addObject:object];
            }

            else
            {

                PFRelation *relation = [object relationForKey:@"whoseTurn"];
                PFQuery *relQuery = [relation query];
                [relQuery findObjectsInBackgroundWithBlock:^(NSArray *userObjects, NSError *error1){


                    NSMutableArray *arr = [NSMutableArray array];
                    for(PFUser *user in userObjects)
                    {
                        [arr addObject:user.objectId];
                    }

                    if([arr containsObject:[PFUser currentUser].objectId])
                    {
                       [self.myTurn addObject:object];
                    }

                    else
                        [self.theirTurn addObject:object];

                }];
            }

        }

        self.allGames = [NSArray arrayWithObjects:self.myTurn, self.theirTurn, self.gameOver, nil];
        [self.tableView reloadData];

    }];

}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [self.allGames count];
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return [[self.allGames objectAtIndex:section] count];
}

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

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];

    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                       reuseIdentifier:MyIdentifier];
    }

    PFObject *object = self.allGames[indexPath.section][indexPath.row];
    cell.textLabel.text = [object objectForKey:@"numPlayers"];

    return cell;
}
5
  • Did you set some breakpoints to check that everything gets effectively called, including code in your subquery? Commented Feb 12, 2015 at 6:04
  • @Romain I had a bunch of log statements that I removed when I pasted the code here. Everything is called, it just seems as though the arrays are empty at the time I create the "allGames" array. Commented Feb 12, 2015 at 6:06
  • 1
    Your Parse queries run asynchronously, and as such, the allGames is initialized with 3 empty arrays at the time you call reloadData. Since your data structure is known (an array comprised of 3 arrays), why not initializing it with 3 empty arrays before running your Parse queries and then, in callback blocks, updating it directly (with proper thread synchronization) and finally calling reloadData? This will end up in calling reloadData much more often, but iOS should be OK with it, and if not, you'll still be able to find optimizations later with your working code anyway. Commented Feb 12, 2015 at 6:23
  • @Romain They are initially empty, but why would they be empty at the time I call reloadData as I placed that statement at the end of the block? Can you post some code for the solution you suggested? Commented Feb 12, 2015 at 6:25
  • That's actually true for self.gameOver, but not for the other ones. Does self.gameOver actually contain at least one object when you call self.allGames = [NSArray arrayWith...];? If not, try to put some data into Parse that would make an object appear in this array. The other two are getting filled in asynchronous blocks, so after the allGames initialization, probably, that's why they are still empty at that point. Commented Feb 12, 2015 at 6:37

1 Answer 1

2

Here's what you could try to do:

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

    PFQuery *gameQuery = [PFQuery queryWithClassName:@"Game"];
    [gameQuery whereKey:@"players" equalTo:[PFUser currentUser]];
    [gameQuery orderByDescending:@"createdAt"];

    [gameQuery findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error){

        @synchronized(self.allGames) {
            self.allGames = [NSArray arrayWithObjects: [NSMutableArray array], [NSMutableArray array], [NSMutableArray array], nil];
            // These two instance variables contain values used in asynchronous blocks, to know how much responses are still expected
            self.expectedNbGames = [objects count];
            self.fetchedNbGames = 0;
        }

        for(PFObject *object in objects)
        {
            if([object objectForKey:@"isOver"] == [NSNumber numberWithBool:YES])
            {
                @synchronized(self.allGames) {
                    [((NSMutableArray *)self.allGames[2]) addObject:object];
                    self.fetchedNbGames++;
                }
            }

            else
            {
                PFRelation *relation = [object relationForKey:@"whoseTurn"];
                PFQuery *relQuery = [relation query];
                [relQuery findObjectsInBackgroundWithBlock:^(NSArray *userObjects, NSError *error1){

                    NSMutableArray *arr = [NSMutableArray array];
                    for(PFUser *user in userObjects)
                    {
                        [arr addObject:user.objectId];
                    }

                    @synchronized(self.allGames) {
                        if([arr containsObject:[PFUser currentUser].objectId])
                        {
                            [((NSMutableArray *)self.allGames[0]) addObject:object];
                        }

                        else
                            [((NSMutableArray *)self.allGames[1]) addObject:object];
                        }

                        self.fetchedNbGames++;
                        if (self.fetchedNbGames == self.expectedNbGames)
                        {
                            // We have now received the last expected response, it's time to sort everything again in descending order based on the "createdAt" value
                            NSSortDescriptor *dateDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"createdAt" ascending:NO];
                            NSArray *sortDescriptors = [NSArray arrayWithObject:dateDescriptor];
                            // self.allGames[0] is already sorted anyway because it's been left untouched from the initial PFQuery
                            self.allGames[1] = [self.allGames[1] sortedArrayUsingDescriptors:sortDescriptors];
                            self.allGames[2] = [self.allGames[2] sortedArrayUsingDescriptors:sortDescriptors];
                            // And... reload one last time!
                            dispatch_async(dispatch_get_main_queue(), ^{
                                [self.tableView reloadData];
                            });
                        }
                    }

                    dispatch_async(dispatch_get_main_queue(), ^{
                        [self.tableView reloadData];
                    });
                }];
            }

        }

        [self.tableView reloadData];
    }];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    @synchronized(self.allGames) {
        return [self.allGames count];
    }
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    @synchronized(self.allGames) {
        return [[self.allGames objectAtIndex:section] count];
    }
}

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

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];

    if (cell == nil)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                       reuseIdentifier:MyIdentifier];
    }

    @synchronized(self.allGames) {
        PFObject *object = self.allGames[indexPath.section][indexPath.row];
        cell.textLabel.text = [object objectForKey:@"numPlayers"];
    }

    return cell;
}

The @synchronized() construct is used to ensure thread-safety, because you will be accessing (both reading and writing) the sell.allGames array from different threads (the main thread, but also asynchronous blocks coming from Parse replies).

Note that we're dispatching the reloadData multiple times from the asynchronous blocks because there is currently no way in your code to know when all the blocks have finished running. We need to dispatch this call on the main thread, because it will trigger UI updates, and those always have to be done on the main thread.

See if this works, and maybe you'll be able to improve it a little bit using properties for example (to avoid writing @synchronized() every time you're using self.allGames).

UPDATE: I've improved my answer by providing a way to re-sort everything after all responses have been received. This process involves the creation of two new instance variables (integers named fetchedNbGames and expectedNbGames) that have to be updated and compared to each other after every response received from Parse. Once all responses have been received, the results array can be re-sorted (it might have been disordered because of requests being sent and handled asynchronously) and the table view refreshed again.

This is untested and there is probably a lot of room for improvements, but it's up to you to tune it to your needs, you get the basic idea.

Also see this other SO thread for a more visual explanation of your problem related to threading and asynchronous calls.

Sign up to request clarification or add additional context in comments.

8 Comments

If I changed my columns in parse to be arrays of PFObjects instead of Relations, I would not have to run the second background query, right? I would just be able to retrieve the array and compare the objects.
I guess so, yes. But your current design probably has its reasons, too. Did you try the solution I provided?
Yes, I am implementing it now, but my internet connection is poor so the queries are not executing.
Edit: This solution seems to be working pretty well! How will the load time be affected when there are say 100 objects returned from the query?
There seem to be some issues arising with this method, now. The objects do not always load in the correct order, even though I specify the order here [gameQuery orderByDescending:@"createdAt"]; any ideas?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.