Sunday, August 01, 2010

Customising the Appearance of UITabBarController

(I tried out a couple of titles for this post, one of which was "Customising the Appearance of UITabBar". But I realised that, while in effect this is how you could describe the problem, it wasn't really accurate. To all intents and purposes, UITabBarController is a single opaque entity. Yes, there's a UITabBar in there somewhere, but that knowledge alone is of little use to you. I get the feeling that UITabBar has its own documentation only because otherwise UITabBarController's would be even longer than it already is.)

The Problem: Whichever finger painter you're currently beholden to has decided that your next app is going to use a tab bar. Only not Apple's tab bar. Oh no. Because that's what ever other app looks like and it just doesn't look designed and anyway they want all seventeen tab buttons to appear at once without that "More" button thing and mwaa mwaa mwaa mwaa mwaa.

So you're left with the task of reimplementing UITabBarController, something which originally took a team of Apple developers about three times as long as you've been allocated for the entire project. And you know that you won't even get close, because you'll loose all that useful Apple goodness like -viewWill(Did)(Dis)Appear messages which actually propagate to sub-view controllers and being able to honour hidesBottomBarWhenPushed from the depths of the navigation stack.

What you'd really like to do is just change how the regular UITabBar displayed by UITabBarController looks.

The Warning: I'm using this code in a production project, but I haven't submitted it to the App Store yet. There's a chance that adopting this approach may get your app rejected. (Although chances are that's more likely to happen bceause your new style tab bar looks too much like Apple's own, rather than because of anything happening at the code level.)

This has been tested on 3.1.3 and 4.0.1, but it's exactly the kind of evil hackery which is likely to break with a future iOS upgrade.

The Solution: We're going to create a UITabBarController subclass (yes, even though Apple says not to in the second line of the docs). Here's the code. And here is an example project (based on the default iPhone tab bar template) for the impatient among you).

SJCTabBarController.h:

#import <uikit/uikit.h>

@interface SJCTabBarController : UITabBarController {
UIView *_fakeTabBar;
UIButton *_currentSelection;
}

@property (nonatomic,assign) IBOutlet UIView *fakeTabBar;

-(IBAction)fakeTabTapped:(id)sender;

@end

SJCTabBarController.m:

#import "SJCTabBarController.h"

@implementation SJCTabBarController

@synthesize fakeTabBar=_fakeTabBar;

// evil trickery happens here
-(UITabBar *)tabBar { return nil; }

-(void)viewDidLoad {
[super viewDidLoad];

// install our fake tab bar
[[super tabBar] addSubview: _fakeTabBar];

// set up the default selected tab
// you may like to read the tab/tag number from the user defaults
[self fakeTabTapped: [_fakeTabBar viewWithTag: 0]];
}

// switch tabs
-(IBAction)fakeTabTapped:(id)sender {
// do we need to do anything?
if(sender == _currentSelection) return;

// un-select the currently selected button
_currentSelection.selected = NO;

// select the new button
_currentSelection = (UIButton *)sender;
_currentSelection.selected = YES;
self.selectedIndex = _currentSelection.tag;

// you may like to write the selected index into user defaults here
}

-(void)viewDidUnload {
[super viewDidUnload];

_fakeTabBar = nil;
_currentSelection = nil;
}

@end

Explanations in a second, but first the Interface Builder part of the equation.

In IB, drag in a UITabBarController (or use the one from the MainWindow.xib created with the default tab bar XCode template) and add view controllers to it. Then change it's class to our UITabBarContoller sub-class. Now add a view to act as your new-look tab bar. This should be a screen wide by the standard 49 points high. Connect it to the fakeTabBar outlet in the subclass. Give it a non-zero tag higher than the number of tabs you want to add.

Now add a button for each tab. Give them a tag which is the same as the index of the view controller you want selected when they're tapped (eg. the first is 0, the second 1, etc.). Wire them up to send -fakeTabTapped: to the sub-class.

You should end up with a hierarchy like this (I've added a total of six view controllers to the tab bar controller in order to demonstrate how awesomely it all works):



Which will produce something like this (which, by the way, is why no designer working with me needs worry about their job security):



So what's going on in this code? It's really rather simple, and yet I couldn't find anything quite like it on the Googles (which suggests that I've done something incredibly stupid and just not noticed it. I bet you have. The comments are down there). Our custom tab bar is added as a subview of the existing tab bar. We access this via [super tabBar] because we've overridden the getter for the tabBar property to return nil. We do this to stop Apple's code (which is well behaved and seems to always use the property accessors rather than going straight to the ivar) from altering what it thinks is a standard tab bar. Comment-out this method and you'll see a ghostly "More" tab and various labels appearing.

One last fix. You'll notice that when you select a tab which should have been managed by the "More" tab, a navigation bar with a back button entitles "More" will automatically appear at the top of the view. You can remove this by calling

[self.navigationController setNavigationBarHidden: YES];

in the -viewWillAppear: method of these view controllers. This has the unfortunate side effect of preventing you from using a navigation controller-managed navigation bar in these controllers, but the chances are that if your designer doesn't want to use the standard tab bar, they won't want this behaviour either.

(Edit: And immediately after posting this I realised that, in -fakeTabTapped:, instead of exiting if the same tab was selected again we should be checking for a navigation controller and popping all of its subviews. I'll leave that as an exercise for the reader.)