Over the last few years, I have been developing iPhone and iPad applications and one of recurring problem is managing how to present information in flexible way that works on both iPhone and iPad. The iOS platform uses UITabBarController to organize controllers, however it also comes with a number of limitations such as it only show five tabs on iPhone and though you can add more tabs but you have to go through “More” tab to access them. Also, it always show the tabs on the bottom and you cannot change their position.
I would show how you can develop a customized tab controller that works on both iPad and iPhone and supports multiple orientations.
Custom Split View Controller
The first thing I needed was to split screen between tab-bars and the main view and though iPad platform supports split view controller (UISplitViewController), however, there are several limitations of the builtin UISplitViewController class such as it only works on iPad in landscape mode and it does not work in iPhone. Here is an example of customized split view controller:
#import "MasterDetailSplitController.h" #import "MenuViewController.h" #import "MenuCellView.h" @implementation MasterDetailSplitController @synthesize menuViewController = _menuViewController; @synthesize detailsViewController = _detailsViewController; @synthesize menuView = _menuView; @synthesize detailsView = _detailsView; - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { } return self; } - (void)dealloc { [_menuViewController release]; [_detailsViewController release]; [_menuView release]; [_detailsView release]; [super dealloc]; } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; isPortrait = YES; self.menuViewController = [[[MenuViewController alloc] initWithSplit:self] autorelease]; self.menuViewController.view.frame = CGRectMake(0, 0, self.menuView.frame.size.width, self.menuView.frame.size.height); [self.menuView addSubview:self.menuViewController.view]; self.menuView.transform = CGAffineTransformMakeRotation(-M_PI * 0.5); self.menuView.autoresizesSubviews = YES; self.menuView.backgroundColor = [UIColor grayColor]; [self layoutSubviews]; } - (void)viewDidUnload { [super viewDidUnload]; self.menuViewController = nil; self.detailsViewController = nil; self.detailsView = nil; } - (void) pushToDetailController:(UIViewController *)controller { if (self.detailsViewController == controller) { return; } if (self.detailsViewController != nil && [self.detailsViewController isKindOfClass:[UINavigationController class]]) { UINavigationController *navCtr = (UINavigationController *)self.detailsViewController; if ([navCtr.viewControllers containsObject:controller]) { return; } } for (UIView *v in self.detailsView.subviews) { [v removeFromSuperview]; } if (![controller isKindOfClass:[UINavigationController class]]) { UINavigationController *navCtr = [[[UINavigationController alloc] initWithRootViewController:controller] autorelease]; [navCtr setDelegate:self]; controller = navCtr; } [self layoutSubviews]; controller.view.frame = CGRectMake(0, 0, self.detailsView.frame.size.width, self.detailsView.frame.size.height); controller.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin; self.detailsView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; self.detailsViewController = controller; [self.detailsView addSubview:controller.view]; } #pragma mark - #pragma mark STANDARD METHODS - (void) viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self.menuViewController viewWillAppear:animated]; [self.detailsViewController viewWillAppear:animated]; } - (void) viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self.menuViewController viewDidAppear:animated]; [self.detailsViewController viewDidAppear:animated]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.menuViewController viewWillDisappear:animated]; [self.detailsViewController viewWillDisappear:animated]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; [self.menuViewController viewDidDisappear:animated]; [self.detailsViewController viewDidDisappear:animated]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; } - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { isPortrait = toInterfaceOrientation == UIInterfaceOrientationPortrait || toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown; if (isPortrait) { self.menuView.transform = CGAffineTransformMakeRotation(-M_PI * 0.5); } else { self.menuView.transform = CGAffineTransformIdentity; } [self.menuViewController willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; [self.detailsViewController willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; } - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { isPortrait = fromInterfaceOrientation != UIInterfaceOrientationPortrait && fromInterfaceOrientation != UIInterfaceOrientationPortraitUpsideDown; [self layoutSubviews]; [self.menuViewController didRotateFromInterfaceOrientation:fromInterfaceOrientation]; [self.detailsViewController didRotateFromInterfaceOrientation:fromInterfaceOrientation]; } - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [self.menuViewController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; [self.detailsViewController willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; } - (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [self.menuViewController willAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; [self.detailsViewController willAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; } - (void)didAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { [self.menuViewController didAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation]; [self.detailsViewController didAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation]; } - (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration { [self.menuViewController willAnimateSecondHalfOfRotationFromInterfaceOrientation:fromInterfaceOrientation duration:duration]; [self.detailsViewController willAnimateSecondHalfOfRotationFromInterfaceOrientation:fromInterfaceOrientation duration:duration]; } #pragma mark - #pragma mark Helpers - (CGSize) sizeRotated { UIScreen *screen = [UIScreen mainScreen]; CGRect bounds = screen.bounds; CGRect appFrame = screen.applicationFrame; CGSize size = bounds.size; float statusBarHeight = MAX((bounds.size.width - appFrame.size.width), (bounds.size.height - appFrame.size.height)); if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) { // we're going to landscape, which means we gotta swap them size.width = bounds.size.height; size.height = bounds.size.width; } size.height = size.height - statusBarHeight -self.tabBarController.tabBar.frame.size.height; return size; } - (void) layoutSubviews { CGSize size = [self sizeRotated]; if (isPortrait) { self.detailsView.frame = CGRectMake(0, 0, size.width, size.height-kMENU_CELL_HEIGHT); self.menuView.frame = CGRectMake(0.0, size.height-kMENU_CELL_HEIGHT, size.width, kMENU_CELL_HEIGHT); } else { self.menuView.frame = CGRectMake(0.0, 0.0, kMENU_CELL_WIDTH, size.height); self.detailsView.frame = CGRectMake(kMENU_CELL_WIDTH, 0, size.width-kMENU_CELL_WIDTH, size.height); } } - (void) loadView { CGSize size = [self sizeRotated]; UIView *view = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)] autorelease]; view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; self.view = view; self.menuView = [[[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, kMENU_CELL_WIDTH, size.height)] autorelease]; self.menuView.autoresizesSubviews = YES; self.menuView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleHeight; self.menuView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:1.000]; self.menuView.contentMode = UIViewContentModeScaleToFill; self.detailsView = [[[UIView alloc] initWithFrame:CGRectMake(kMENU_CELL_WIDTH, 0, size.width-kMENU_CELL_WIDTH, size.height)] autorelease]; self.detailsView.autoresizesSubviews = YES; self.detailsView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.detailsView.backgroundColor = [UIColor colorWithWhite:1.000 alpha:1.000]; self.detailsView.contentMode = UIViewContentModeScaleToFill; [self.view addSubview:self.menuView]; [self.view addSubview:self.detailsView]; } @end
You can customize offset of split controller by setting values for kMENU_CELL_WIDTH and kMENU_CELL_HEIGHT. Note that split view controller transforms the menu-view controller and rotates it 90″ when orientation changes to portrait so that table can be scrolled horizontally.
Building Menu Controller, that can scroll vertically and horizontally
Next, you need a UITableViewController, which can scroll horizontal and vertically. You can use transform to change the view such as:
#import "MenuViewController.h" #import "SampleTableController.h" #import "Configuration.h" #import "MenuCellView.h" #import "MenuInfo.h" #import "CustomNavigationController.h" @implementation MenuViewController @synthesize menuItems = _menuItems; @synthesize masterDetailSplitController = _masterDetailSplitController; - (id)initWithSplit:(MasterDetailSplitController*)split { self = [super initWithStyle:UITableViewStylePlain]; if (self) { self.masterDetailSplitController = split; self.tableView.separatorColor = [UIColor clearColor]; self.menuItems = [[[NSMutableArray alloc] initWithCapacity:10] autorelease]; } return self; } -(void)refresh { MenuInfo *menu = nil; [self.menuItems removeAllObjects]; CustomNavigationController *settingsCtr = [[[CustomNavigationController alloc] initWithStyle:UITableViewStylePlain] autorelease]; menu = [[[MenuInfo alloc] initWithLabel:@"Settings" andOnImage:@"onSettings.png" andOffImage:@"offSettings.png" andController:settingsCtr] autorelease]; [self.menuItems addObject:menu]; NSArray *orderList = [[Configuration sharedConfiguration] getNavigationOrder]; for (NSString *order in orderList) { MenuKind kind = (MenuKind) [order intValue]; SampleTableController *ctr = [[[SampleTableController alloc] initWithStyle:UITableViewStylePlain] autorelease]; switch (kind) { case ONE: ctr.heading = @"One"; menu = [[[MenuInfo alloc] initWithLabel:@"One" andOnImage:@"onDown.png" andOffImage:@"offDown.png" andController:ctr] autorelease]; break; case TWO: ctr.heading = @"Two"; menu = [[[MenuInfo alloc] initWithLabel:@"Two" andOnImage:@"onDownLeft.png" andOffImage:@"offDownLeft.png" andController:ctr] autorelease]; break; case THREE: ctr.heading = @"Three"; menu = [[[MenuInfo alloc] initWithLabel:@"Three" andOnImage:@"onDownRight.png" andOffImage:@"offDownRight.png" andController:ctr] autorelease]; break; case FOUR: ctr.heading = @"Four"; menu = [[[MenuInfo alloc] initWithLabel:@"Four" andOnImage:@"onLeft.png" andOffImage:@"offLeft.png" andController:ctr] autorelease]; break; case FIVE: ctr.heading = @"Five"; menu = [[[MenuInfo alloc] initWithLabel:@"Five" andOnImage:@"onNext.png" andOffImage:@"offNext.png" andController:ctr] autorelease]; break; case SIX: ctr.heading = @"Six"; menu = [[[MenuInfo alloc] initWithLabel:@"Six" andOnImage:@"onPrevious.png" andOffImage:@"offPrevious.png" andController:ctr] autorelease]; break; case SEVEN: ctr.heading = @"Seven"; menu = [[[MenuInfo alloc] initWithLabel:@"Seven" andOnImage:@"onRight.png" andOffImage:@"offRight.png" andController:ctr] autorelease]; break; case EIGHT: ctr.heading = @"Eight"; menu = [[[MenuInfo alloc] initWithLabel:@"Eight" andOnImage:@"onUp.png" andOffImage:@"offUp.png" andController:ctr] autorelease]; break; case NINE: ctr.heading = @"Nine"; menu = [[[MenuInfo alloc] initWithLabel:@"Nine" andOnImage:@"onUpLeft.png" andOffImage:@"offUpLeft.png" andController:ctr] autorelease]; break; case TEN: ctr.heading = @"Ten"; menu = [[[MenuInfo alloc] initWithLabel:@"Ten" andOnImage:@"onUpRight.png" andOffImage:@"offUpRight.png" andController:ctr] autorelease]; break; } [self.menuItems addObject:menu]; } selected = 0; [self.tableView reloadData]; } - (UIViewController *) selectedController { MenuInfo *info = [self.menuItems objectAtIndex:selected]; return info.controller; } - (void)dealloc { [_masterDetailSplitController release]; [_menuItems release]; [super dealloc]; } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; isPortrait = YES; UIView *bgView = [[[UIView alloc] initWithFrame:CGRectZero] autorelease]; bgView.transform = CGAffineTransformMakeRotation(M_PI * 0.5); self.tableView.backgroundView = bgView; self.view.backgroundColor = [UIColor blackColor]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.tableView.showsVerticalScrollIndicator = NO; self.tableView.showsHorizontalScrollIndicator = NO; self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine; self.tableView.separatorColor = [UIColor clearColor]; [self refresh]; [self.masterDetailSplitController pushToDetailController:[self selectedController]]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; } -(NSArray *)allControllers { NSMutableArray *list = [[[NSMutableArray alloc] initWithCapacity:10] autorelease]; for (MenuInfo *info in self.menuItems) { [list addObject:info.controller]; } return list; } - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { isPortrait = toInterfaceOrientation == UIInterfaceOrientationPortrait || toInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown; for (UIViewController *ctr in [self allControllers]) { [ctr willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; } [self.tableView reloadData]; } - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { for (UIViewController *ctr in [self allControllers]) { [ctr didRotateFromInterfaceOrientation:fromInterfaceOrientation]; } } - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { for (UIViewController *ctr in [self allControllers]) { [ctr willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; } } - (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { for (UIViewController *ctr in [self allControllers]) { [ctr willAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation duration:duration]; } } - (void)didAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { for (UIViewController *ctr in [self allControllers]) { [ctr didAnimateFirstHalfOfRotationToInterfaceOrientation:toInterfaceOrientation]; } } - (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration { for (UIViewController *ctr in [self allControllers]) { [ctr willAnimateSecondHalfOfRotationFromInterfaceOrientation:fromInterfaceOrientation duration:duration]; } } #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.menuItems count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"MenuCell"; MenuCellView *cell = (MenuCellView *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[MenuCellView alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil] autorelease]; } MenuInfo *menu = [self.menuItems objectAtIndex:indexPath.row]; NSString *imageName = selected == indexPath.row ? menu.onImage : menu.offImage; cell.imageView.image = [UIImage imageNamed:imageName]; if (isPortrait) { cell.transform = CGAffineTransformMakeRotation(M_PI * 0.5); } else { cell.transform = CGAffineTransformIdentity; } return cell; } /** * Displays the specified controller view */ -(void) displayControllerView:(UIViewController*)controller { if (controller != nil) { [self.masterDetailSplitController pushToDetailController:controller]; } [self.tableView reloadData]; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { selected = indexPath.row; UIViewController *controller = [self selectedController]; [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES]; [self displayControllerView:controller]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { return kMENU_CELL_WIDTH * 3; } else { return kMENU_CELL_WIDTH; } } @end
The height of cells for iPad is stretched so that you can test scrolling across the screen. The cells are also rotated 90″ when orientation is in portrait mode so that they are displayed properly on the horizontal table. Also, if you have a background image, then you will need to rotate it as well.
Customizing order of tabs
You can also allow users to change the order by storing order in a configuration, here is an example of controller that allows reordering:
#import "CustomNavigationController.h" #import "MenuViewController.h" #import "Configuration.h" #import "AppDelegate.h" @implementation CustomNavigationController @synthesize controllers; static NSArray *kControllerNames; + (void) initialize { kControllerNames = [[NSArray arrayWithObjects:@"One", @"Two", @"Three", @"Four", @"Five", @"Six", @"Seven", @"Eight", @"Nine", @"Ten", nil] retain]; } - (id)initWithStyle:(UITableViewStyle)style { self = [super initWithStyle:style]; if (self) { } return self; } #pragma mark - View lifecycle - (void)viewDidLoad { [super viewDidLoad]; controllers = [[NSMutableArray alloc] initWithCapacity:10]; [super setEditing:YES animated:YES]; [self.tableView setEditing:YES animated:YES]; self.navigationItem.title = @"Navigation"; self.tableView.backgroundColor = [UIColor clearColor]; [self.tableView setIndicatorStyle:UIScrollViewIndicatorStyleWhite]; self.tableView.separatorColor = [UIColor blackColor]; } -(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [controllers removeAllObjects]; NSArray *list = [[Configuration sharedConfiguration] getNavigationOrder]; for (NSString *ctr in list) { [controllers addObject:[NSNumber numberWithInt:[ctr intValue]]]; } [self.tableView reloadData]; } - (void)viewDidUnload { [super viewDidUnload]; self.controllers = nil; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [controllers count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"CustomNavigationCellView"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } NSNumber *num = [controllers objectAtIndex:indexPath.row]; cell.textLabel.text = [kControllerNames objectAtIndex:[num intValue]]; return cell; } #pragma mark - Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } - (UITableViewCellEditingStyle)tableView:(UITableView *)aTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewCellEditingStyleNone; } - (void)tableView:(UITableView *)aTableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { [self.tableView reloadData]; } #pragma mark Row reordering - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { NSNumber *item = [[controllers objectAtIndex:fromIndexPath.row] retain]; [controllers removeObject:item]; [controllers insertObject:item atIndex:toIndexPath.row]; [item release]; NSMutableArray *list = [[[NSMutableArray alloc] initWithCapacity:10] autorelease]; for (NSNumber *num in controllers) { [list addObject:[num stringValue]]; } [[Configuration sharedConfiguration] setNavigationOrder:list]; AppDelegate *delegate = (AppDelegate *) [[UIApplication sharedApplication] delegate]; [delegate.splitViewController.menuViewController refresh]; } - (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath { return NO; } -(void)dealloc { [controllers release]; [super dealloc]; } @end
Note that we are storing the order of tabs in the configuration and saving it when user moves the navigation items.
Screen shots
(Note that iPad images are stretched to show that they are horizontal and vertical scrollable)
Download the code
I am not showing all code here but the full code is available from https://github.com/bhatti/SplitControllerAndCustomNavigation. I hope you find it useful.