Technology Musings

iOS - using storyboard relationships for custom view controller containment

JB

One thing that iOS forgot to do with the release of storyboards and iOS 5 is support for custom "relationship" segues.  In addition to normal segues, UINavigationController and UITabBarController have what are called "relationships", which allow you to define containment information for these controllers.  So, for the navigation controller, you define the root view controller, and for the tab bar controller, you define the list of view controllers.  This is all done in a neat interface in the storyboard editor (which is the replacement for interface builder).

Another cool feature in iOS is "containment", which is basically a way of letting you write view controllers which contain other view controllers.  This was not really allowed in iOS 4, but they've added some basic support for it.  Unfortunately, the way that you might want to use containment with storyboarding is through relationships, but iOS doesn't provide a way to do custom relationships in storyboards - or even their default ones on custom view controllers.

My own task was to create a tab bar controller with the tabs at the *top*.  I had a few ideas on how I might approach this problem.  Subclassing UITabBarController didn't seem like a good idea.  So I tried several others.

One possible way was duck typing.  I thought that perhaps I could pull a fast on on the storyboard editor.  What I would do is define my own class that mimicked UITabBarController without actually inheriting from it.  Then, I could create my relationships with the storyboard editor, and then substitute my own non-tab-bar controller at the last minute.  This failed miserably.  

Firts of all, the storyboard editor doesn't like you to substitute non-subclasses for the class that it wants.  If you try to set it to a non-subclass of UITabBarController, it won't give you an error, but it won't save the change, either.

I tried to work around this by making my class inherit from UITabBarController while I was editing the storyboard, and then change it back for the compile.  This worked momentarily, but in the end, it turns out that the "relationship" segues aren't set via getters and setters.  I imagine there is an internal API that is being used.

Therefore, I next tried overriding initWithCoder and reading the nib values myself.  Unfortunately, I was not successful at guessing the keys for decoding.

Then, it hit me.  What I need to do is to mix subclassing UITabBarController with not mixing UITabBarController.  So here's what I did:

class 1: MyTabBarController

This is just a shell class, but it inherits from UITabBarController.  

class 2: MyTabBarControllerImplementation

This is your real implementation class, and only needs to inherit from UIViewController.  Make it work like you want it to.

What you are going to do is designate your tab bar controllers to be of type MyTabBarController in the storyboard editor.  Then, when viewDidLoad is called on MyTabBarController, the view controller de-registers the view controllers from itself, creates an instance of MyTabBarControllerImplementation, moves the view controllers over there, and then replaces itself with MyTabBarControllerImplementation.

Anyway, here's the code.  It's really rough, but works at the moment.  I'm open to improvements:

MyTabBarController.h:

#import <UIKit/UIKit.h>
@interface MyTabBarController : UITabBarController
@end

MyTabBarController.m:

#import "MyTabBarController.h"
#import "MyTabBarControllerImplementation.h"
@implementation JBTabController
- (void)viewDidLoad
{
    [super viewDidLoad];
    JBTestContainerViewController *vc = [[JBTestContainerViewController alloc] init];
    NSArray *vclist = self.viewControllers;
    [self setViewControllers:[NSArray array] animated:NO];
    vc.viewControllers = vclist;
    NSMutableArray *navlist = [self.navigationController.viewControllers mutableCopy];
    [navlist replaceObjectAtIndex:(navlist.count - 1) withObject:vc];    
    [self.navigationController setViewControllers:navlist animated:NO];
}
@end

MyTabBarControllerImplementation.h:

 

#import <UIKit/UIKit.h>
@interface MyTabBarControllerImplementation : UIViewController<UITabBarDelegate>
@property (nonatomic) int selectedIndex;
@property (strong, nonatomic) NSArray *viewControllers;
@property (strong, nonatomic) UITabBar *bar;
@end

MyTabBarControllerImplementation.m:

 

#import "MyTabBarControllerImplementation.h"
@implementation MyTabBarControllerImplementation
@synthesize viewControllers = _viewControllers;
@synthesize selectedIndex = _selectedIndex;
@synthesize bar = _bar;
-(CGRect) subviewFrame {
    CGRect f = self.view.frame;
    return CGRectMake(0, 30, f.size.width, f.size.height - 30);
}
-(void) reloadBarInfo {
    NSMutableArray *itms = [NSMutableArray arrayWithCapacity:_viewControllers.count];
    for(UIViewController *vc in _viewControllers) {
        UITabBarItem *itm = [[UITabBarItem alloc] initWithTitle:vc.title image:nil tag:0];
        [itms addObject:itm];
        [vc didMoveToParentViewController:self];
    }
    _bar.items = itms;    
    _bar.selectedItem = [itms objectAtIndex:0];
    _bar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    UIViewController *firstVC = [_viewControllers objectAtIndex:0];
    firstVC.view.frame = [self subviewFrame];
    firstVC.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view addSubview:firstVC.view];
}
-(void) viewDidLoad {
    self.bar = [[UITabBar alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 30) ];
    for(UIViewController *vc in _viewControllers) {
        [self addChildViewController:vc];
    }
    _bar.delegate = self;
    [self.view addSubview:_bar];
    [self reloadBarInfo];
}
-(void) tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item {
    int idx = [_bar.items indexOfObject:item];
    UIViewController *newVC = [_viewControllers objectAtIndex:idx];
    UIViewController *curVC = [_viewControllers objectAtIndex:_selectedIndex];
    if(newVC == curVC) {
        return; // Don't switch - same one selected!
    }
    [curVC.view removeFromSuperview];
    newVC.view.frame = [self subviewFrame];
    newVC.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view addSubview:newVC.view];
    [self transitionFromViewController:curVC toViewController:newVC duration:0 options:0 animations:nil completion:nil];
     self.selectedIndex = idx;
}
@end

And there you have it!