Blog Archives

/** Customizing moreNavigationController */

An iPhone application can display a maximum of 5 tabs at once. If your application has more than 5 tabs, the infamous moreNavigationController comes into play. This is a subclass of UINavigationController that displays a table of those tabs in a UITabBarController that are not visible on the bottom bar because, well, the way I look at it, you have way too many tabs in your design. 😛

But personal opinion aside, there are times when you (willingly or otherwise) may have to grapple with this controller. I had to recently, and let me tell you, it was not fun finding info on this. So, if you are a fellow hunter/gatherer/coder, rummaging around all those infinite StackOverFlow posts for bits of information on customizing this elusive controller, I hereby present a not-so-perfect guide for taming the beast:

Our goal is to create a basic demo app that looks as follows:

tab0

tab1

customMNC

customConfigure

As always, begin by creating a new XCode project and add a new UIViewController file to the project. I shall call mine MyViewController. Always original, moi.

newFile1 newFile2

So now that we have a controller, we can add some background color along with a label and an NSInteger property, tabNumber, to help us distinguish between the controllers:

#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController
{
    UILabel *tabDetail;
}

@property (nonatomic) NSInteger tabNumber;

- (id)initWithNumber:(NSInteger)number;

@end

The ViewController.m should look like this:

@implementation MyViewController

@synthesize tabNumber;

- (id)initWithNumber:(NSInteger)number
{
    self = [super init];
    if (self) {
        tabNumber = number;
        [self.view setBackgroundColor:[UIColor colorWithRed:189.0f/255.0f green:219.0f/255.0f blue:168.0f/255.0f alpha:1.0f]];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    //Get the screen dimensions
    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    CGSize screenSize = screenBounds.size;

    tabDetail = [[UILabel alloc] initWithFrame:CGRectMake(screenSize.width/2 - 50.0f , 15.0f, 100.0f, 30.0f)];
    [tabDetail setBackgroundColor:[UIColor clearColor]];
    [tabDetail setTextAlignment:NSTextAlignmentCenter];
    [tabDetail setTextColor:[UIColor whiteColor]];
    [tabDetail setFont:[UIFont fontWithName:@"MarkerFelt-Thin" size:25.0f]];
    [tabDetail setText:[NSString stringWithFormat:@"Tab %d", tabNumber]];

    [self.view addSubview:tabDetail];
}

@end

Import the MyViewController.h file into your AppDelegate.m file. We now have to initialize the tab bar controller and add multiple navigation controllers to it. Each of these navigation controllers will have a MyViewController as its root controller. For simplicity, I use a loop and an NSMutableArray. To change the look of the navigation bars, I am assigning a background image. Alternatively, you can change the tintColor of the bar. Thanks to UIAppearance, changing the properties of navigation bars is now a one line job. At this point, this is how the appDidFinishLaunching method will look:

#import "AppDelegate.h"
#import "MyViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    UITabBarController *tabBarController = [[UITabBarController alloc] init];

    NSMutableArray *viewControllers = [[NSMutableArray alloc] init];

    for (int i =0; i<11; i++) {
        MyViewController *VC = [[MyViewController alloc] initWithNumber:i];
        [VC.navigationItem setTitle:[NSString stringWithFormat:@"Navigation Bar %d",i]];
        [VC.tabBarItem setTitle:[NSString stringWithFormat:@"Tab %d",i]];
        [VC.tabBarItem setImage:[UIImage imageNamed:@"notebook.png"]];//image for tab icon

        UINavigationController *NC = [[UINavigationController alloc] initWithRootViewController:VC];
        [NC.navigationBar setBackgroundImage:[UIImage imageNamed:@"navigationBarBG.png"] forBarMetrics:UIBarMetricsDefault];

        [viewControllers addObject:NC];
    }

    [[UINavigationBar appearance] setTitleTextAttributes:
     [NSDictionary dictionaryWithObjectsAndKeys:
      [UIColor colorWithRed:189.0f/255.0f green:219.0f/255.0f blue:168.0f/255.0f alpha:1.0f], UITextAttributeTextColor,
      [UIFont fontWithName:@"MarkerFelt-Thin" size:16.0f], UITextAttributeFont,nil]];

    [tabBarController setViewControllers:viewControllers];

    [self.window setRootViewController:tabBarController];

    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
 }

Go ahead and run the app on a simulator/device. Click through the tabs one by one and you will notice that while all the tabs themselves are just as they should be, the More tab (which contains a UITableView as shown below) has a mind of its own:

uncustomMNC1

So, this is where the fun part starts. We will now try to make the More tab as consistent with the other tabs as possible. This becomes so much simpler if we work on one feature at a time. First off, we will tackle the navigation bar. For some reason, UIAppearance does not seem to affect the moreNavigationController’s navigation bar, so we have to explicitly set the background image. Add the highlighted lines to your application delegate:

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    .
    .
    .

    //Add this line to change the moreNavigationController's navigation bar image:
    [tabBarController.moreNavigationController.navigationBar setBackgroundImage:[UIImage imageNamed:@"navigationBarBG.png"] forBarMetrics:UIBarMetricsDefault];

    //You can change the navigation bar title by adding the following line:
    [[tabBarController.moreNavigationController.viewControllers objectAtIndex:0] setTitle:@"Navigation Bar for More Tab"];

     [self.window setRootViewController:tabBarController];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

Run the app now and you will see that the look and feel of the navigation bar for the More tab is now consistent with that of the rest of the navigation bars:

uncustomMNC2

However, the Edit button looks completely out of place here. Sometimes, you may wish to disable editing altogether. In that case, all you have to do to is to set the customizableViewControllers property of the UITabBarController to nil as follows:

    //I'm not doing this here, but you can if you like. This disables the 'Edit' button:
    tabBarController.customizableViewControllers = nil;

I happen to like the Edit option so I will customize the button instead. To do this, we have to implement a UINavigationControllerDelegate method and set our AppDelegate as the delegate for the moreNavigationController. If you are not aware of how delegates work in Objective-C, I strongly recommend that you read up on them. I am a huge fan of delegation because it makes life that much simpler. For now though, you should at least understand that, “a delegate allows one object to send messages to another object when an event happens.“ If you are from a Java background, I like to think of a delegate simply as an event listener.

So first off, set the AppDelegate as a UINavigationControllerDelegate, UITabBarControllerDelegate and UITableViewDataSource. We need UINavigationControllerDelegate methods to customize the Edit button, UITabBarControllerDelegate methods to make changes to the Configure screen(which pops up when you tap Edit) and UITableViewDataSource methods to make changes to the moreNavigationController‘s table cells. This is what the AppDelegate.h must look like now:

@interface AppDelegate : UIResponder <UIApplicationDelegate, UINavigationControllerDelegate, UITabBarControllerDelegate, UITableViewDataSource>
{
    id originalDataSource;
}
@property (strong, nonatomic) UIWindow *window;

@end

Then, in AppDelegate.m, set the moreNavigationController‘s delegate to self and implement the navigationController:willShowViewController:animated: method as follows:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    UITabBarController *tabBarController = [[UITabBarController alloc] init];

    .
    .
    .

   //Add this line to modify the 'Edit' button
    [tabBarController.moreNavigationController setDelegate:self];

    [self.window setRootViewController:tabBarController];
     self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
 }

- (void)navigationController:(UINavigationController *)navigationController  willShowViewController:(UIViewController *)viewController  animated:(BOOL)animated
{
    UINavigationBar *moreNavBar = navigationController.navigationBar;
    UINavigationItem *moreNavItem = moreNavBar.topItem;
    moreNavItem.rightBarButtonItem.tintColor = [UIColor whiteColor]; // Set color
    [moreNavItem.rightBarButtonItem setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
                                                            [UIColor colorWithRed:57.0f/255.0f green:95.0f/255.0f blue:35.0f/255.0f alpha:1.0f], UITextAttributeTextColor,
                                                            [UIFont fontWithName:@"MarkerFelt-Thin" size:16.0f], UITextAttributeFont,nil] forState:UIControlStateNormal];
    [moreNavItem.rightBarButtonItem setTitle:@"Re-arrange"];// Set edit button title

}

If you run the app again, you will notice that the Edit button now has its shine on. ☺

uncustomMNC3

Next, we will tackle the table view that lists our “extra” tabs. This is easy to do once you understand that the topViewController property of the moreNavigationController gives you access to the UITableViewController. We can now modify the UITableViewController’s view like any other UITableView:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    UITabBarController *tabBarController = [[UITabBarController alloc] init];

    .
    .
    .

    //Set title
    [[tabBarController.moreNavigationController.viewControllers objectAtIndex:0] setTitle:@"Navigation Bar for More Tab"];

    //Set background
    [tabBarController.moreNavigationController.topViewController.view setBackgroundColor: [UIColor colorWithRed:189.0f/255.0f green:219.0f/255.0f blue:168.0f/255.0f alpha:1.0f]];

    //Set table separator style
    [(UITableView *)tabBarController.moreNavigationController.topViewController.view setSeparatorStyle:UITableViewCellSeparatorStyleNone];

    [self.window setRootViewController:tabBarController];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

But this code above will only change the background and seperator of the table.

uncustomMNC4

What if we wish to change table cell details like font, text color, etc? Well, there are 2 ways to go about this:

  1. The first method is shorter and gives us limited control over the table cells. Simply access the cells via the visibleCells property of the table. But there are some things like removing accessoryView which are downright impossible to do this way, since as soon as you scroll the table it will redraw its cells all over again!
    //Avoid using this method - it is unreliable
    for (UITableViewCell *cell in [(UITableView *)tabBarController.moreNavigationController.topViewController.view visibleCells]) {
    
            [cell.textLabel setFont:[UIFont fontWithName:@"MarkerFelt-Thin" size:20.0f]];
            [cell.textLabel setTextColor:[UIColor whiteColor]];
            [cell setAccessoryType:UITableViewCellAccessoryNone];
            [cell setAccessoryView:nil];
        }
  2. The second method involves implementing the UITableViewDataSource protocol. We create a variable to hold the original data source of the moreNavigationController, assign the AppDelegate as the data source for the moreNavigationController and implement UITableViewDataSource methods as below. This ensures that even if the user scrolls the moreNavigationController’s table, the cells will be redrawn just the way we want them.
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    
        UITabBarController *tabBarController = [[UITabBarController alloc] init];
    
        .
        .
        . 
    
        [tabBarController.moreNavigationController.topViewController.view setBackgroundColor: [UIColor colorWithRed:189.0f/255.0f green:219.0f/255.0f blue:168.0f/255.0f alpha:1.0f]];
        [(UITableView *)tabBarController.moreNavigationController.topViewController.view setSeparatorStyle:UITableViewCellSeparatorStyleNone];
    
        //This way, the cells will be redrawn correctly on scrolling
        [tabBarController.moreNavigationController setDelegate:self];
        originalDataSource = [(UITableView *)tabBarController.moreNavigationController.topViewController.view dataSource];
        [(UITableView *)tabBarController.moreNavigationController.topViewController.view  setDataSource:self];
    
        [self.window setRootViewController:tabBarController];
        self.window.backgroundColor = [UIColor whiteColor];
        [self.window makeKeyAndVisible];
        return YES;
     }
    
    -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        return [originalDataSource tableView:tableView numberOfRowsInSection:section];
    }
    
    -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
         UITableViewCell *cell = [originalDataSource tableView:tableView cellForRowAtIndexPath:indexPath];
         cell.accessoryType = UITableViewCellAccessoryNone;   
        [cell.textLabel setFont:[UIFont fontWithName:@"MarkerFelt-Thin" size:20.0f]];
        [cell.textLabel setTextColor:[UIColor whiteColor]];
        [cell.imageView setAlpha:0.5f];
    
       return cell;
    }

customMNC

All that remains now is to fix the Configure controller:

uncustomMNC5

To do so, set , we assign the AppDelegate to be the tab bar controller’s delegate and implement the tabBarController:willBeginCustomizingViewControllers: method. Some folks claim this may not work all the time. This code works on both the simulator and my iPhone 5 test device, but I would still advise caution when implementing this.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    UITabBarController *tabBarController = [[UITabBarController alloc] init];

    .
    .
    .

    originalDataSource = [(UITableView *)tabBarController.moreNavigationController.topViewController.view dataSource];
    [(UITableView *)tabBarController.moreNavigationController.topViewController.view  setDataSource:self];

    //This helps us customize the 'Configure' screen
    [tabBarController setDelegate:self];

    [self.window setRootViewController:tabBarController];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

- (void)tabBarController:(UITabBarController *)controller willBeginCustomizingViewControllers:(NSArray *)viewControllers {

    UIView *editViews = [controller.view.subviews objectAtIndex:1];
    UINavigationBar *editModalNavBar = [editViews.subviews objectAtIndex:0];

    [editModalNavBar setBackgroundImage:[UIImage imageNamed:@"navigationBarBG.png"] forBarMetrics:UIBarMetricsDefault];

     UINavigationItem *modalNavItem =editModalNavBar.topItem;
    modalNavItem.rightBarButtonItem.tintColor = [UIColor whiteColor]; //Set color.
    [modalNavItem.rightBarButtonItem setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
                                                            [UIColor colorWithRed:57.0f/255.0f green:95.0f/255.0f blue:35.0f/255.0f alpha:1.0f], UITextAttributeTextColor,
                                                            [UIFont fontWithName:@"MarkerFelt-Thin" size:16.0f], UITextAttributeFont,nil] forState:UIControlStateNormal];
    [modalNavItem.rightBarButtonItem setTitle:@"Save Changes"];//Set bar button title.

    [[controller.view.subviews objectAtIndex:1] setBackgroundColor:[UIColor colorWithRed:189.0f/255.0f green:219.0f/255.0f blue:168.0f/255.0f alpha:1.0f]];

    editModalNavBar.topItem.title = @"Re-arrange Tabs";//Set title
}

customConfigure

Well, that’s it then. Build and see for yourself – not only the moreNavigationController’s table but also the Configure view looks much better, IMHO.  These are all hacks and tricks I’ve gathered after searching the internet for my last application, so suggestions and comments are always welcome.

Happy coding! ☺

Advertisements