Monday 31 December 2012

1000 Apps Downloaded!

MDF Projects has officially had its 1000th download. The 1000th app was Velocimeter, my speedometer app. It's been a crazy first year of iOS development, and I'm excited to embark on the second one. Also, Happy New Year!

Saturday 29 December 2012

GKHelper: getting GKPlayer data

I'm going to start going over useful methods from my code in order to help people with similar problems, and give myself a form of self assessment. The first method I'm going to go over is the method I use to get GKPlayer objects from player IDs. This code is from my forthcoming turn based Game Center game (name TBD).

For all my Game Center interactions, I have created a singleton manager, named GKHelper. It handles creating the async calls to Game Center, and processes the information when it comes back. It also has several forms of caching to reduce web requests.

GKHelper has a dictionary which it uses to store GKPlayers by playerID. Whenever a GKPlayer object is fetched, it is first stored in this dictionary before being returned. Whenever a GKPlayer is requested via playerID, the dictionary is checked before the request is made. This logic is handled via the following method:


- (GKPlayer*)playerForPlayerID:(NSString*)playerID {
   if(playerID == nil || [playerID isEqualToString:@""]) {
      return nil;
   }
   if(self.playersDict == nil) {
      self.playersDict = [NSMutableDictionary dictionary];
   }
   
   if([self.playersDict objectForKey:playerID]) {
      return [self.playersDict objectForKey:playerID];
   } else {
      [GKPlayer loadPlayersForIdentifiers:[NSArray arrayWithObject:playerID] withCompletionHandler:^(NSArray *players, NSError *error) {
         
         if (error != nil) {
            NSLog(@"Error retrieving player info: %@", error.localizedDescription);
         } else {
            // Populate players dict
            for (GKPlayer *player in players) {
               [self.playersDict setObject:player forKey:player.playerID];
            }
            [self.delegate performSelectorOnMainThread:@selector(playersLoaded) withObject:nil waitUntilDone:NO];
         }
      }];
   }
   return nil;
}


First, the method checks the playerID to make sure it is valid. Then, it checks the dictionary for the specified player. On the case that it cannot find the player, it begins the async web request for the player data, which it then stores upon return. The delegate is then informed on the main thread (since it may return on a background thread) that the players list has been updated (the delegate usually reloads the data in the tableview). After the web request is sent, the function returns nil indicating the player could not be found. The function will get called a second time upon receiving the player info, and the correct information will be retrieved. This function is costly, however, since the async request takes an array of playerIDs, and we're only passing in a single ID.

That's where the next two functions come in.


- (void)getPlayersForMatch:(GKTurnBasedMatch*)match {
   
   NSMutableArray *identifiers = [NSMutableArray array];
   
   for(GKTurnBasedParticipant *participant in match.participants) {
      if(participant.playerID == nil) {
         continue;
      }
      [identifiers addObject:participant.playerID];
   }
   
   [GKPlayer loadPlayersForIdentifiers:identifiers withCompletionHandler:^(NSArray *players, NSError *error) {
      
      if (error != nil) {
         NSLog(@"Error retrieving player info: %@", error.localizedDescription);
      } else {
         // Populate players dict
         for (GKPlayer *player in players) {
            [self.playersDict setObject:player forKey:player.playerID];
         }
         [self.delegate playersLoaded];
      }
   }];
}

- (void)getPlayersForMatches:(NSArray*)matches {
   NSMutableArray *identifiers = [NSMutableArray array];
   
   for(GKTurnBasedMatch *match in matches) {
      for(GKTurnBasedParticipant *participant in match.participants) {
         if(participant.playerID == nil) {
            continue;
         }
         [identifiers addObject:participant.playerID];
      }
   }
   
   [GKPlayer loadPlayersForIdentifiers:identifiers withCompletionHandler:^(NSArray *players, NSError *error) {
      
      if (error != nil) {
         NSLog(@"Error retrieving player info: %@", error.localizedDescription);
      } else {
         // Populate players dict
         for (GKPlayer *player in players) {
            [self.playersDict setObject:player forKey:player.playerID];
         }
         [self.delegate playersLoaded];
      }
   }];
}


These two functions take a GKTurnBasedMatch, and simply fill up the player dictionary with the GKPlayer objects. The first method I call when a new match is created, and the second method is called when the app is first opened, right after the current matches are loaded. They insure that the loadPlayers method is only called once at app loading, and once per new match. There are a few problems with this setup although.

For one, the latter two methods have no way of communicating with the former method regarding making async web requests. In other words, getPlayersForMatches could be called, immediately followed by playerForPlayerID. This would start two async web requests for the same information, with potentially bad results. There's also the case where the async web request fails to get a certain GKPlayer for a playerID, but still calls the delegate's playersLoaded method. Thus, an infinite loop will be formed.

If I ever get around to optimizing these methods, I'll post the new ones below. Also, make sure to comment if you have any questions. Also, sorry about the code formatting, haven't figured out how to make it format properly yet...

Tuesday 11 December 2012

Lego Trains are awesome

Found my old Lego train, so I set it up around the tree. Here are some pictures.




AirBoard Overview

AirBoard is a handy app I made to keep score during spontaneous tournaments that sometimes occur. The app is compatible with an Apple Tv, so when you mirror your iOS device's screen on the ATV, it shows the overall scoreboard.

This allows you to edit the list on the fly, and the secondary screen will update once you're finished. I'm planning to add a boatload of features to this app, such as saving a scoreboard, creating a tournament bracket from a scoreboard, and possibly inter-device communication. The UI assets do need an upgrade though...

Another cool feature is the ability to name the scoreboard by typing directly on the navigation bar.

Screenshots below: