It is a little thing about UIViewController, but it has always bothered me - the boilerplate code needed to setup some of a view controller's default properties (e.g. the tab bar item and navigation item) and the location of this code.
Apple's documentation says the following:
tabBarItem
The tab bar item that represents the view controller when added to a tab bar controller.
@property(nonatomic, retain) UITabBarItem *tabBarItem
Discussion
This is a unique instance of UITabBarItem created to represent the view controller when it is a child of a tab bar controller. The first time the property is accessed, the UITabBarItem is created. Therefore, you shouldn’t access this property if you are not using a tab bar controller to display the view controller. To ensure the tab bar item is configured, you can either override this property and add code to create the bar button items when first accessed or create the items in your view controller’s initialization code.
The default value is a tab bar item that displays the view controller’s title.
The default implementation is helpful, but it's certainly not enough. So where does one setup the remaining properties of the tab bar item instance that is lazily provided by UIViewController?
I've seen (and done) just about every possibility, but the highlights:
1. Set the property from the "upstream" object.
If you build out a tab bar controller in code (say from your App Delegate), then it is a real temptation to setup the tab bar items. You're right there setting up the controller, and in your brain it is easy to group the tab bar item setup process as part of that. However, I fully endorse this approach is "wrong." The tab bar item is part of and managed by a view controller.
2. In the init method of your subclass.
This certainly seems logical: in order for your view controller to function you'll need a tab bar item, and so why not go ahead and set it up when you create the view controller? Well, because that defeats the lazy loading and couples your view controller with knowledge that it is part of a tab bar controller. What happens when the layout changes and that view controller actually gets pushed onto a navigation stack? Will someone remember to refactor the tab bar item code out of init? I also endorse this as "wrong" because it doesn't follow the intention of lazy loading the property. Apple's documentation suggests this is a viable option, but maybe they are implying...
3. In -[UIViewController viewDidLoad]
I'd wager this is the most common place to do the setup. I know I've written the following lines of code a couple dozen times:
if (self.tabBarController) {
UITabBarItem *tbi = [self tabBarItem];
[tbi setImage:[UIImage imageNamed:@"tab-bar-item-icon"]];
[tbi setTitle:NSLocalizedString(self.title, @"Tab Bar Item - View Controller 1")];
[tbi setBadgeValue:@""];
[self setTabBarItem:tbi];
}
My problem with this approach is it doesn't really make sense why it needs to be in viewDidLoad. The property is already lazy loaded when a the tab bar controller asks it's contained view controllers for their tab bar items. This scatters the responsibility of creating and configuring the item across non-related methods. Which leads me to the final solution...
4. Override the lazy load accessor method
Apple suggests this in their documentation, and stylistically follows the design they chose. Okay, so, how does one accomplish such a feat? There are a couple of solutions I've come up with, and I'd like to know if there are any others (that may be more elegant). Both of these seem a little too clever, which sends of alarm bells inside my head.
Option 1
Back the property with an instance variable writable by your subclass.
@implementation ViewControllerA
{
UITabBarItem *_viewControllerATabBarItem;
}
- (UITabBarItem *)tabBarItem
{
if (!_viewControllerATabBarItem) {
_viewControllerATabBarItem = [super tabBarItem];
[_viewControllerATabBarItem setImage:[UIImage imageNamed:@"tab-bar-item-icon"]];
[_viewControllerATabBarItem setTitle:NSLocalizedString(self.title, @"Tab Bar Item - View Controller 1")];
[_viewControllerATabBarItem setBadgeValue:@""];
}
return _viewControllerATabBarItem;
}
- (void)setTabBarItem:(UITabBarItem *)tabBarItem
{
if (tabBarItem != _viewControllerATabBarItem) {
_viewControllerATabBarItem = tabBarItem;
}
}
@end
The necessity to back the property with an instance variable in our subclass is due to the fact Apple have declared the _tabBarItem instance variable with @package. This approach is perfectly reasonable, however, it certainly doesn't reduce boilerplate! Also, it costs a mostly unused _tabBarItem instance variable's resources by calling super. You certainly could alloc and init a UITabBarItem, but this might make your code brittle with future changes to the UIViewController class.
Option 2
Use Key-Value Coding to access the instance variable directly
- (UITabBarItem *)tabBarItem
{
if (![self valueForKey:@"_tabBarItem"]) {
UITabBarItem *tbi = [super tabBarItem];
[tbi setImage:[UIImage imageNamed:@"tab-bar-item-icon"]];
[tbi setTitle:NSLocalizedString(self.title, @"Tab Bar Item - View Controller 1")];
[tbi setBadgeValue:@""];
[self setTabBarItem:tbi];
}
return [self valueForKey:@"_tabBarItem"];
}
This is less boilerplate and doesn't have a need for a second instance variable, but seems like clever shenanigans that will make future-me angry. It certainly works, but is there a more straight forward way?
I'm curious to get feedback on my thoughts and proposed solutions, and also see other implementations. It's such a small thing and seems silly to think about given how many possible solutions there are, but there doesn't seem to be a clear cut better-than-all-the-others solution. So, how do you handle such a mundane detail?
NOTE: I've left the discussion of XIBs and Storyboard out of possible solutions because Navigation Items and Tab Bar Items have some states you cannot influence from Interface Builder. E.g. tab bar item's setFinishedSelectedImage:withFinishedUnselectedImage:
or navigation items's leftItemsSupplementBackButton
. If your app doesn't need these, I fully endorse using Interface Builder to setup these properties!