Monday, May 7, 2012

UIStoryboards and UITabBarController

Previously I wrote about decomposing UIStoryboards into modules. One of the most natural modules are the tabs of a tab bar controller. Since segues can't cross storyboards, we can't use a UITabBarController in a storyboard. This means the UITabBarController must be written in code. I have put together an easy pattern for building UITabBarControllers in code.

UIStoryboard Category

The first step is to write a category on UIStoryboard. It will look something like this:

@implementation UIStoryboard (YourExtras)

+ (UIStoryboard *)searchStoryboard {
    return [UIStoryboard storyboardWithName:@"SearchStoryboard" bundle:nil];
}

+ (UIStoryboard *)mapStoryboard {
    return [UIStoryboard storyboardWithName:@"MapStoryboard" bundle:nil];
}

+ (UIStoryboard *)listStoryboard {
    return [UIStoryboard storyboardWithName:@"ListStoryboard" bundle:nil];
}

+ (UIStoryboard *)favoritesStoryboard {
    return [UIStoryboard storyboardWithName:@"FavoritesStoryboard" bundle:nil];
}

+ (UIStoryboard *)contactStoryboard {
    return [UIStoryboard storyboardWithName:@"ContactStoryboard" bundle:nil];
}

+ (UIStoryboard *)homeStoryboard {
    return [UIStoryboard storyboardWithName:@"HomeStoryboard" bundle:nil];
}

@end

The purpose of this category is to abstract away the storyboard file names. Should the storyboard name (or bundle) change, we will only need to change it in one place. Such methods are useful not just tabs but also for any storyboard module. For the purpose of tabs, each storyboard generally should have a UINavigationController as its initial view.

UITabBarController Category

The next step involves a category I wrote. It takes in an array of UIStoryboards, then instantiates each storyboard as a tab. It saves writing a little bit of code between each project.

@implementation UITabBarController (RBExtras)

+ (UITabBarController *)tabBarControllerWithStoryboardTabs:(NSArray *)tabs {

    UITabBarController * tabBarController = [UITabBarController new];
    NSMutableArray * instantiatedTabs = [NSMutableArray arrayWithCapacity:[tabs count]];

    // Instantiates each of the storyboards.
    [tabs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * stop) {

        NSAssert([obj isKindOfClass:[UIStoryboard class]],
                          @"Expected UIStoryboard, got %@",
                          NSStringFromClass([obj class]));

        [instantiatedTabs addObject:[obj instantiateInitialViewController]];
    }];

    [tabBarController setViewControllers:instantiatedTabs];

    return tabBarController;
}

@end

This category can be found on my Github.

App Delegate Superclass

Next is an app delegate superclass I wrote. This saves a few more lines of code from each tab-based app.

@implementation RBAppDelegate

@synthesize window = _window;

- (void)setUpTabBasedAppWithTabs:(NSArray *)tabs block:(RBTabBarCustomizationBlock)block {

    NSAssert(tabs, @"No tabs given.");
    NSAssert([tabs count] >= 2, @"Insufficient number of tabs.");

    // Sets up the tab bar controller.
    UITabBarController * tabBarController = [UITabBarController tabBarControllerWithStoryboardTabs:tabs];

    // The block is called to allow customization of the tab bar controller.
    if (block) block(tabBarController);

    // Sets up the window.
    UIWindow * window = [UIWindow new];
    [window setScreen:[UIScreen mainScreen]];
    [window setRootViewController:tabBarController];
    [window makeKeyAndVisible];
    [self setWindow:window];
}

@end

This superclass completes the set up of the tab bar controller. It provides a block for any customizations of the tab bar controller. You can also find this on my Github.

Your App Delegate

The final step is to call the set up method. Don't forget to subclass RBAppDelegate first.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // Creates the tabs.
    NSArray * tabs = [NSArray arrayWithObjects:
                                  [UIStoryboard mapStoryboard],
                                  [UIStoryboard listStoryboard],
                                  [UIStoryboard searchStoryboard],
                                  [UIStoryboard favoritesStoryboard],
                                  [UIStoryboard contactStoryboard],
                                  nil];

    [self setUpTabBasedAppWithTabs:tabs
                                                   block:
     ^(UITabBarController * tabBarController) {
         tabBarController.tabBar.selectedImageTintColor = [UIColor greenColor];
     }];

    return YES;
}

The block for customizing the tab bar controller is optional, but I have shown the potential use of such a block. In this case I am setting the color of the selected tab icons.

Conclusion

Within a few lines of code, we can have a tab-based app constructed from several storyboards. Other UIStoryboard patterns can be created using similar techniques.