Sunday, May 22, 2011

UITabBarController with custom UITabBarItem colors


In the last few days my team and I were tasked with creating an app for a brand which has orange colors. We wanted a tab bar. We wanted the selected tab bar item to highlight in orange rather than the default aqua color. If you're reading this than you probably already know that a straightforward way to achieving this using UITabBarController doesn't exist. In this post I'd like to go through the code I've written for a custom tab bar controller that allows you to set custom image masks for your highlighted items. The project hasn't been implemented to fully replicate the functionality of UITabBarController, however the basics are in and you can add additional features if you like.


What does the project consist of?


The project consists of 3 classes as shown below.


How do you use it?


Very simple. You use it just like you would use UITabBarController with two exceptions:

  1. If you're loading your current tab bar controller from a NIB file you will need to set it up programatically.
  2. You need to set up the image masks used for selected and unselected items.

So where do you start? Download the project code here. Copy the files from the TATabBarController folder into your project and change all references of UITabBarController to TATabBarController. Once you've done that you're left with going through 4 easy steps to get things up and running.



Don't forget to set the tabBarItem property for the view controllers you're adding to the tab bar controller - otherwise nothing will appear.


How does it work?


Once you set the view controllers the magic begins to happen. Below I'll describe how I went about drawing the tab bar images and text. The rest of the details regarding the functioning of the tab bar will be left out of the scope of this post. The code that shows how all this is done is located in TATabBars - (UIImage*)_tabBarImage:(UIImage*)tabBarImage withGradient:(UIImage*)gradientImage method.



Additionally, if we are drawing the selected tab bar item we apply a shadow to the image and text.


You can download the project code here. Have fun!

Saturday, April 30, 2011

Game Center invitation errors. Two things often overlooked when dealing with GKInvite.

The past few days I've been working on integrating Snooker Club with most of Game Kit's functionality (leaderboards, achievements, multiplayer, voice chat). Having done this before it went like a breeze. The multiplayer capabilities add a lot to the fun and playability of the game... if they work. In certain situations the game wouldn't connect with other players, it wouldn't even acknowledge that an invite had been received. It didn't take long to sort it out, but perhaps I can save you a little bit of time by outlining the 2 most common scenarios that are overlooked in Apple's Game Kit guide.


Scenario 1


Inviting a friend works fine when you're testing? The app is off, you get an invite, the app launches and manages to connect. Funnily enough, when testing on a larger user base it would seem that when friends in close proximity want to challenge each other they tend to wait for their invites whilst GKMatchmakerViewController is already being displayed. In the example Apple provides your code for handling an invite should look like this:


[GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) {
   // Insert application-specific code here to clean up any games in progress.
   if (acceptedInvite)
    {
        GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:acceptedInvite] autorelease];
        mmvc.matchmakerDelegate = self;
        [self presentModalViewController:mmvc animated:YES];
    }
    else if (playersToInvite)
    {
        GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
        request.minPlayers = 2;
        request.maxPlayers = 4;
        request.playersToInvite = playersToInvite;
 
        GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
        mmvc.matchmakerDelegate = self;
        [self presentModalViewController:mmvc animated:YES];
    }
};

The code above doesn't take into account that you might already be displaying an instance of GKMatchmakerViewController as a modal view. Trying to display a new instance of GKMatchmakerViewController initialized with the new invite and displaying it as a modal view controller leads to nothing. In order to handle this particular scenario you need to dismiss the visible modal view controller before presenting the new one, like so:


[GKMatchmaker sharedMatchmaker].inviteHandler = ^(GKInvite *acceptedInvite, NSArray *playersToInvite) {
    UIViewController *topLevelViewController = (UIViewController*)[[(GameHostAppDelegate*)[UIApplication sharedApplication] delegate] gameHostViewController];
    // Insert application-specific code here to clean up any games in progress.

    if (acceptedInvite)
    {
     isInviter = NO;
     GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithInvite:acceptedInvite] autorelease];
     mmvc.matchmakerDelegate = self;
     
     if([topLevelViewController modalViewController] != nil)
      [topLevelViewController dismissModalViewControllerAnimated:NO];

     [topLevelViewController presentModalViewController:mmvc animated:YES];
    }
    else if (playersToInvite)
    {
     isInviter = NO;
     GKMatchRequest *request = [[[GKMatchRequest alloc] init] autorelease];
     request.minPlayers = 2;
     request.maxPlayers = 2;
     request.playersToInvite = playersToInvite;
     
     GKMatchmakerViewController *mmvc = [[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease];
     mmvc.matchmakerDelegate = self;
     
     if([topLevelViewController modalViewController] != nil)
      [topLevelViewController dismissModalViewControllerAnimated:NO];

     [topLevelViewController presentModalViewController:mmvc animated:YES];
    }



Make sure you don't animate the view controller being dismissed, otherwise the new instance of GKMatchmakerViewController will not show up.


Scenario 2


You've applied the fix for the issue mentioned above. Things work much better, but still feel a little unreliable? Try this.

  • Launch your app.
  • Put your app into the background.
  • Send an invite to yourself from another device.
  • Accept the invite.

If the invite is handled correctly go through the steps again, this time on a slower connection. Did you notice how unreliable your invitation handler has become? Apple's documentation states the following: Important:Your application should provide an invitation handler as early as possible after your application authenticates the local player; an appropriate place to set the handler is in the completion handler that executes after the local player is authenticated. It is critical that your application authenticate the local player and set the invitation handler as soon as possible after launching, specifically so that you can handle the invitation that caused your application to be launched.

There is a very important thing to extract from Apples info. Your application should provide an invitation handler as early as possible after your application authenticates the local player. You don't want to have an invitation handler in place if your user is not authenticated. This becomes a bit tricky with multitasking.

If you're on a device that supports multitasking you need to authenticate every time your app enters the foreground (as of iOS 4.2 this is handled automatically). You're probably already doing something like:

- (void)applicationWillEnterForeground:(UIApplication *)application
{
 [gameCenter validateAuthentication];
}

where validateAuthentication checks to see if you're still logged in as the same player, that you're authenticated and if not tries to authenticate you.

This leads to an interesting situation, which results in unreliable handling of invites. Look at the 4 steps above. When you accept an invitation, your app goes into the foreground. The [GKMatchmaker sharedMatchmaker].inviteHandler is called but the player has most probably not been authenticated yet. Nothing happens. Whats the solution, you ask? Simple.

In your application's delegate method - (void)applicationDidEnterBackground:(UIApplication *)application simply add this line:

[GKMatchmaker sharedMatchmaker].inviteHandler = nil;

This way the invite handler won't be called until the player is authenticated. Once the player is authenticated the inviteHandler will be created once again, assuming you've followed Apple's guides and added the code to create the handler after the user authenticates ( in [localPlayer authenticateWithCompletionHandler:^(NSError *error) ).

I hope that the solutions to these 2 common scenarios bring you closer to solving reliability issues with Game Center.

Saturday, February 19, 2011

NSBundle vs. UINib performance

Amongst many wonders in iOS 4.0 you will find a class named UINib. It's purpose - to optimise the loading time of nibs. It has been rumored to be much faster than NSBundle. In this post I set out to compare the loading times for nib files using good ol' NSBundle (or to be more precise the methods provided by the "UINibLoadingAdditions" NSBundle category) and the younger UINib.


I've created a simple test case in which a table view displays cells that consist of 2 labels. The cell has been designed in Interface Builder and saved in it's very own nib file. Once the table view asks for a cell we load the nib file, instantiate the object graph and voilĂ ! We end up with a beautiful table view displaying 11 cells. This also means that my table view will need to create 11 instances of my cell. I better find the most optimal way to load that nib!


I start out by using the NSBundle loading mechanism. In my - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method I load the nib, measure the time it took to load, find the table view cell and set some values for its 2 labels.

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

if(cell == nil)
{
NSDate *startDate = [NSDate date];
NSArray *bundleObjects = [[NSBundle mainBundle] loadNibNamed:@"ExampleCell" owner:nil options:nil];
printf("%f\n", [[NSDate date] timeIntervalSinceDate:startDate]);

for(id obj in bundleObjects)
{
if([obj isKindOfClass:[UITableViewCell class]])
cell = (UITableViewCell*)obj;
}
}

[(UILabel*)[cell viewWithTag:1] setText:[NSString stringWithFormat:@"%d", [indexPath section]]];
[(UILabel*)[cell viewWithTag:2] setText:[NSString stringWithFormat:@"%d", [indexPath row]]];

return cell;
}


I ran the code 3 times on an iPhone 4. On average the time it took to load each of the 11 cells in milliseconds was:

0.016110333
0.006223667
0.004825
0.004784333
0.005055667
0.004709333
0.004510333
0.004458667
0.004389
0.004378333
0.004712667

Notice how the first loading takes a considerably larger amount of time than all the next ones. I haven't found any information mentioning NSBundle doing any sort of caching so I'm presuming the reason why every subsequent call takes less time is due to the underlying file system methods it uses.



Next, I replaced NSBundle with UINib like so:


if(cell == nil)
{
NSDate *startDate = [NSDate date];
if(!nib_)
nib_ = [UINib nibWithNibName:@"ExampleCell" bundle:nil];

NSArray *bundleObjects = [nib_ instantiateWithOwner:nil options:nil];

printf("%f\n", [[NSDate date] timeIntervalSinceDate:startDate]);

for(id obj in bundleObjects)
{
if([obj isKindOfClass:[UITableViewCell class]])
cell = (UITableViewCell*)obj;
}
}


Notice how the UINib has the loading and instantiation methods seperated. This provides an immense performance boost, because we only have to load the nib file from disk once and with each subsequent call to the table views delegate method only instantiate a new object graph using the cached nib data.

Same as before, I ran this 3 times and here is what I got:

0.015249333
0.002955
0.002447
0.002064667
0.002092667
0.001903
0.001851667
0.001981
0.001852667
0.001873333
0.001941667

Again, the first loading time is much higher than the subsequent ones.



So how do the 2 compare?



We can see that both start out at a similar point. With each subsequent call both UINib and NSBundle loading times remain at their respective levels. It's at this point where it becomes obvious that UINib beats NSBundle hands down. The fact that UINib keeps nib data in memory proves to be it's winning point if you're instantiating the same nib more than once.