Ruby on Rails, iOS, Git...

We spend a lot of time thinking about these things. If we have something helpful to share, we'll put it here.

Request a free RaddOnline® proposal.

iPhone SDK: UITabBarController, How to Save User Customized Tab Order

Posted by Tim Stephenson, RaddOnline® on Friday, February 27, 2009

Even with Apple's iPhone SDK, There's No Free Lunch.

With the UITabBarController Apple has provided tons of functionality for free. One thing that the UITabBarController does not do is save the order of the tabs when a user changes them, so we have to do that task by ourselves.

UITabBarController Class: Quick Background

The UITabBarController class in the iPhone SDK enables users to move between multiple view controllers with a click of a tab at the bottom of the screen. If you have more than five tabs in your application, it also offers a More tab. When tapped, it displays the additional tabs in a table for users to select them. When in the More tab of the application, clicking on the "Edit" button brings up a view where users can customize the order of the tabs to suit their own needs.

While they can re-order the tabs, unless we take a few steps in our code, their settings won't be saved.

Overview: Saving Customized Tab Order

  1. When the application quits, save the current order of the tabs to an array in the user defaults.
  2. The next time it opens, retrieve the saved order of the tabs from the defaults.
  3. Loop through the view controllers and place them into an array with the saved order.
  4. Provide the newly ordered array to the UITabBarController for display.

I'm going to go step by step through the process with an example that does not use Interface Builder to define the UITabBarController only because it makes it a little easier to understand how the array of controllers is created.  I did the example both ways so both example projects are attached.

UITabBarController Project Setup

I created a new project in Xcode using the Window Based Applicaiton template. Because I'm a little lazy, I opted to reuse the same controller and view in each of the tabs. The view has a single label in it that is changed to match the title of each tab.

  1. Right click on the classes group and select add new file.
  2. Select Cocoa Touch Classes, and then select UIViewController Subclass and give it a name. I named mine AllTabsViewController.
  3. In the AllTabsViewController.h file add the following code:
    @interface AllTabsViewController : UIViewController {
    IBOutlet UILabel *nameLabel;
    }
    @property (nonatomic, retain) UILabel *nameLabel;
    @end
  4. Right click on resources folder in Xcode and add a new file. This time select User Interfaces and then select View XIB. I named my nib file AllTabsView.xib.
    filesowner-class
  5. Open the new nib file, select the File's Owner icon and then change the class to AllTabsViewController in the Identity inspector. You should see the nameLabel outlet in the Class Outlets portion of the screen.
  6. Drag a label from the library onto the view. I centered mine and made it nice and big.
  7. ib-connection-tabsControl click on the File's Owner icon and drag from the nameLabel outlet to the label we added. Also drag from the view outlet to the view in our nib file.
  8. When done, close InterfaceBuilder.

Start by Adding Tabs to the Tab Bar Controller

Next we'll set up the application to build the tabs and load the controllers.

  1. Back in Xcode, open the AllTabsViewContrller.m file to finish the implementation of the view controller.
  2. First, synthesize our property for the label:
    @synthesize nameLabel;
  3. While we're at it, let's go ahead and make sure it get's released to:
    - (void)dealloc {
    [nameLabel release];
    [super dealloc];
    }
  4. The only other thing to do here is change the label when the view loads. Go to the viewDidLoad method and add the following code:
    - (void)viewDidLoad {
    nameLabel.text = self.title;
    [super viewDidLoad];
    }
  5. Now open the app delegete class header file, in my case it is called TabBarSaveStateAppDelegate.h. We need to make it a UITabBarController delegate and add a tabBarController porperty. When done it should look like this:
    #import <UIKit/UIKit.h>
    #import "AllTabsViewController.h"

    @interface TabBarSaveStateAppDelegate : NSObject <UIApplicationDelegate, UITabBarControllerDelegate> {
    UIWindow *window;
    UITabBarController *tabController;
    }

    @property (nonatomic, retain) IBOutlet UIWindow *window;
    @property (nonatomic, retain) UITabBarController *tabController;
    @end
  6. Finally, edit the implementation file to populate our tab controller. Open TabBarSaveStateAppDelegate.m and add the following code.
    @implementation TabBarSaveStateAppDelegate

    @synthesize window;
    @synthesize tabController;

    - (void)applicationDidFinishLaunching:(UIApplication *)application {
    tabController = [[UITabBarController alloc] init];
    tabController.delegate = self;

    //Add some tabs to the controller...
    AllTabsViewController *firstTabController = [[AllTabsViewController alloc] initWithNibName: @"AllTabsView" bundle: nil];
    firstTabController.title = @"Tab One";

    AllTabsViewController *secondTabController = [[AllTabsViewController alloc] initWithNibName: @"AllTabsView" bundle: nil];
    secondTabController.title = @"Tab Two";

    AllTabsViewController *thirdTabController = [[AllTabsViewController alloc] initWithNibName: @"AllTabsView" bundle: nil];
    thirdTabController.title = @"Tab Three";

    AllTabsViewController *fourthTabController = [[AllTabsViewController alloc] initWithNibName: @"AllTabsView" bundle: nil];
    fourthTabController.title = @"Tab Four";

    AllTabsViewController *fifthTabController = [[AllTabsViewController alloc] initWithNibName: @"AllTabsView" bundle: nil];
    fifthTabController.title = @"Tab Five";

    AllTabsViewController *sixthTabController = [[AllTabsViewController alloc] initWithNibName: @"AllTabsView" bundle: nil];
    sixthTabController.title = @"Tab Six";

    tabController.viewControllers = [NSArray arrayWithObjects:firstTabController, secondTabController, thirdTabController, fourthTabController, fifthTabController, sixthTabController, nil];

    [firstTabController release];
    [secondTabController release];
    [thirdTabController release];
    [fourthTabController release];
    [fifthTabController release];
    [sixthTabController release];

    [window addSubview:tabController.view];

    // Override point for customization after application launch
    [window makeKeyAndVisible];
    }


    - (void)dealloc {
    [window release];
    [tabController release];
    [super dealloc];
    }
    This code instantiates six view controllers and gives each a unique title. The title will appear on the tab, and on the label in the view itself.

    It then adds each of the view controllers to an array called viewControllers that is a property of the UITabBarController class. Finally it adds the view to a subview of the window.

You should be able to build and run the application. Click on the More tab, edit the order of the tabs. If you close and re-open the application, you should notice that the order you set for the tabs is lost. The tabs will be in the default order. We'll take care of that next.

Saving the Tab Order When the Application Closes

Now that our sample is all set up, I'll take care of the problem I set out to solve in the first place. We'll be doing all this in the AppDelegate class. Open TabBarSaveStateAppDelegate.m again.

  1. The application delegate provides a method that will be called when the application terminates called applicationWillTerminate:. We'll use it to save the state our our tabs. Add this code to the file:
    - (void)applicationWillTerminate:(UIApplication *)application {

    NSMutableArray *savedOrder = [NSMutableArray arrayWithCapacity:6];
    NSArray *tabOrderToSave = tabController.viewControllers;
    for (UIViewController *aViewController in tabOrderToSave) {
    [savedOrder addObject:aViewController.title];
    }

    [[NSUserDefaults standardUserDefaults] setObject:savedOrder forKey:@"savedTabOrder"];
    }

We create two arrays. One that holds the current list of view controllers called tabOrderToSave, and one that stores an array of strings that identify each of our controllers, savedOrder. When I created the view controllers I gave each of them a title. We'll use that title to identify the controllers when the application opens. 

You have to be careful here. If the title were being set during a viewWillAppear method for example, this would not work. The title would not be available. Another strategy would be to use a tag, or to explicitly store a property in the controller that could be used as an identifier. For this purpose, the title works fine.

The final line saves the array into the standard user defaults and gives it a key that we can use to recover it later.

Restoring the Order of the Tabs in the Tab Bar Controller

For this step, I'm going to add a helper method to the app delegate class.

  1. Back in the TabBarSaveStateAppDelegate.h file, add this method to the interface.
    - (void)setTabOrderIfSaved;
  2. Then implement it in the implementation file:
    - (void)setTabOrderIfSaved {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSArray *savedOrder = [defaults arrayForKey:@"savedTabOrder"];
    NSMutableArray *orderedTabs = [NSMutableArray arrayWithCapacity:6];

    if ([savedOrder count] > 0 ) {
    for (int i = 0; i < [savedOrder count]; i++){
    for (UIViewController *aController in tabController.viewControllers) {
    if ([aController.title isEqualToString:[savedOrder objectAtIndex:i]]) {
    [orderedTabs addObject:aController];
    }
    }
    }
    tabController.viewControllers = orderedTabs;
    }
    }
    What's going on? First we get a reference to the standard user defaults. The second line restores the array that was saved when the application closed. Then we set up two loops. The outer loop goes through the saved order, and the inner loops checks the title of the controller against the string that was saved into the savedOrder array. When it gets a match, the controller is added to a new array. Finally, the new array is assigned to the tabController.viewControllers property.
  3. To finish up, just call the new method before the tab controller is added to the window's subview.
    ...

    [self setTabOrderIfSaved];

    [window addSubview:tabController.view];

    // Override point for customization after application launch
    [window makeKeyAndVisible];
    }

You should now be able to build and run the application. Reorder the tabs and close the application. When you open it the next time, they should be in the custom order.

Using Interface Builder - Start With UITabBarController Project Template

I did the same tasks using a interface builder. To create the project I used the UITabBarController project template. There were several differences.

  1. Titles have to be set in Interface Builder. I gave each controller a title in interface builder by clicking on the tab in the MainWindow.xib file and entering the title in the in the title field view controller inspector.
  2. The class for each controller has to be assigned in interface builder both in the main window file and in the second view file.
  3. As I added tabs in interface builder, I set each tab to use the SecondView nib.
  4. In the applicationWillTerminate: method I used the tabBarItem.title property for my identifier like this:
    - (void)applicationWillTerminate:(UIApplication *)application {

    NSMutableArray *savedOrder = [NSMutableArray arrayWithCapacity:6];
    NSArray *tabOrderToSave = tabBarController.viewControllers;

    for (UIViewController *aViewController in tabOrderToSave) {
    [savedOrder addObject:aViewController.tabBarItem.title];
    }

    [[NSUserDefaults standardUserDefaults] setObject:savedOrder forKey:@"savedTabOrder"];
    }

Sample Xcode Projects

Sample project: TabBarSaveState - The non-interface builder version

Sample project: TabBarSaveStateIB - The interface builder version.

Comments

Matt Williamson said on Thursday, March 12, 2009:

Great post! The only issue is that the interface guidelines in the SDK say to never use more than 5 tabs in a UITabBarController, because the icons can be hard to click.

Tim Stephenson said on Friday, March 13, 2009:

Yes, to do this or not is an interesting question. The YouTube and iPod apps use more than 5 tabs very effectively.

Nathan w said on Friday, April 10, 2009:

This is great! I have been having problems with tabs and this has helped me a lot! Thank you!

Elliot said on Wednesday, April 22, 2009:

Awesome piece of code, however I was wondering if its possible to set up one of the tab views to also have buttons that link and load other views (i.e the ones not visible on the tab bar controller) when the application is started. Kind of like a main menu?

Chad said on Saturday, May 02, 2009:

Thank you very much! I added an additional loop at the end of setTabOrderIfSaved: that checks to see whether there are additional Controllers beyond those saved in defaults (useful during development):

for (UIViewController *aController in tabBarController.viewControllers) {
if (![orderedTabs containsObject:aController]) {
[orderedTabs addObject:aController];
}
}

Dennis said on Sunday, September 13, 2009:

Hi Tim. Extremely useful post. Is there an update for iPhone OS v3.1? This works great for me in v2.x, but not on OS v3.x. Thanks.

Jim said on Friday, September 18, 2009:

Instead of title:
aViewController.title
you can use instead:
aViewController.tabBarItem.tag

and then you set the title dynamically for each view controller.

applewhy said on Thursday, September 24, 2009:

superb!! Thanks for the tutorial.

I have been having problems with tab ordering and I just fix it. Thank you!! ;)

worked with 3.0 and 3.1 all fine :)

dev said on Tuesday, October 20, 2009:

hello Tim,

Thank you for the wonderful tutorial, it really helped me. I have another query, basically when I click on any one of extra tab bar items that remain under the more tab, I am able to see its table view but when I further select a row in that table view, the app crashed instead of going to the detail view. However if I use the same tab bar item when it is not inside the more tab , it works as it is supposed to. Could you please guide me how to resolve this?

Gabriel Pachecoby said on Saturday, December 19, 2009:

Thanks dude It's works perfectly , but I modified a lit by creating those tabs at IB and named then at viewDidLoad ,and made yours way to save then but calling the method atdidEndCustomizingViewControllers!
Cheers

Alan D. said on Thursday, December 31, 2009:

MADE OF AWESOME! Thanks! I spent an hour or two trying to figure out how to do this, then I finally broke down and googled, which found me a page which pointed me to yours. ;-) 10 minutes later, my app stores its tab bar item order!

Vipul said on Friday, February 12, 2010:

It worked ... Simple and easy way to implement this functionality

Antony Basta said on Wednesday, July 14, 2010:

hello, i think this is a great tutorial. i made an app with the more tab in the UITabBar. And everything works. except your method of saving and displaying the saved contents of the tab bar don't work on iOS4. PLEASE PLEASE PLEASE post an update on how to imply this in 4.0

PLEASE!

Terry said on Wednesday, July 28, 2010:

Antony,
I am also developing on iOS 4. In general, the code works just as expected. The only thing you have to change is the point at which the save gets done.

Think about it -- under iOS 4 apps typically never have "applicationWillTerminate" called. Instead they sleep. If you, like me, originally tried to test this by closing an app (which puts it to sleep) then killing the sleeping app, this results in the app stopping WITHOUT applicationWillTerminate ever being called.

So, basically, the updated tab order never gets saved!

So when should the save code get called? I added it to the uitabbarcontroller delegate "tabBarController:didEndCustomizingViewControllers:changed:" method as the perfect spot for it.

Note -- I'm not sure why, but I found that adding "[[NSUserDefaults standardUserDefaults] synchronize];" immediately after saving the values helped ensure that things got written out while debugging. I think exiting the app via the debugger might sometimes kill it too quickly for the values to get written out.

Terry

Xuan Chen said on Friday, July 30, 2010:

This is a great tutorial. It helps me a lot. Thanks.

It works for me on iPod Touch with iOS4.0. I do not know why it does not work for some other users.

However there are two other issues: First, if I modified my application and add one more tab, it will not show the last tab. Second, if the user preference file is corrupted, the user will end up with a non-functional app.

I think the loading function needs a little modification. Here is what I did:

// assume all view controllers have been created and added to UITabBarController *tbc;

- (void)setTabOrderIfSaved {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *savedOrder = [defaults arrayForKey:@"savedTabOrder"];
NSMutableArray *orderedTabs = [NSMutableArray arrayWithCapacity:20];

if (savedOrder!=nil && [savedOrder count] > 0 ) {
for (int i = 0; i < [savedOrder count]; i++){
for (UIViewController *aController in tbc.viewControllers) {
if ([aController.title isEqualToString:[savedOrder objectAtIndex:i]]) {
[orderedTabs addObject:aController];
}
}
}
for (UIViewController *aController in tbc.viewControllers) {
int loaded=0;
for (UIViewController *bController in orderedTabs) {
if ([aController.title isEqualToString:bController.title]) {
loaded=1;
break;
}
}
if (loaded==0) {
[orderedTabs addObject:aController];
}
}

tbc.viewControllers = orderedTabs;
}
}

Basically it will add those view controllers in tbc but not in the user default preference after those in user default preference have been added.

victorvj said on Wednesday, September 08, 2010:

Does anyone knows how to change put your own images in the tabbaritem in the non-interface builder version? I don't know how to change it. I have 3 view controllers and the code that controlls the tabbar is inside the appdelegate file

I leave here an extract of my code:

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

// Override point for customization after app launch.

navController = [[UINavigationController alloc] init];
navController2 = [[UINavigationController alloc] init];
navController3 = [[UINavigationController alloc] init];


tabBarController.viewControllers = [NSArray arrayWithObjects:navController, navController2, navController3, nil];


UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@"Grados" style:UIBarButtonItemStyleBordered target:nil action:nil];

// First view controller

MapViewController *mapViewController = [[MapViewController alloc] init];
mapViewController.title = @"Mapa";
mapViewController.navigationItem.backBarButtonItem = backButton;

[navController pushViewController:mapViewController animated:NO];

[mapViewController release];

...
...
...

// Add the tab bar controller's current view as a subview of the window
[window addSubview:tabBarController.view];
[window makeKeyAndVisible];

return YES;
}

Thanks!

Luke said on Wednesday, October 27, 2010:

in the iOS4 in order to have the NSUserDefaults correctly saved is necessary to force the saving on disk through the command:

[defaults synchronize];

Puzant said on Tuesday, December 14, 2010:

Hi , is there a way to keep the tab order the same after you close the process in 4.0 and above.

sales tips said on Saturday, December 18, 2010:

Thanks ! Supper Post !

Karim Souman said on Monday, February 28, 2011:

This does not work with iOS 4+
any help???

Devang Vyas said on Wednesday, March 02, 2011:

Thank you for the tutorial. For me the aViewController.title was always null so I had to use aViewController.nibName

And I also prefer to synchronize in didEndCustomizingViewControllers itself.

I can confirm that it works in iOS4 with minor changes.