Starter Step

Changing a UINavigationController’s Root View Controller

Posted on: March 5, 2009

If you have ever dealt with using a UINavigationController in your iPhone app, you may have wanted to swap out the root view controller with a new one. Unfortunately the iPhone SDK doesn’t allow you to do this. To do so you actually have to create a brand new UINavigationController, which for us was not acceptable.

To support swapping out the root view controller, we created a custom class that extends UINavigationController and adds a method “setRootViewController”. We do a little hack by creating a generic UIViewController and setting it as a fake root view controller at index 0. This allows you to push and pop UIViewControllers above it on the stack. So now the “perceived” root view controller is always at index 1 behind the scenes, but actually appears to be at index 0. It behaves exactly as the original UINavigationController except that now you can change the “perceived” root view controller. Here is an example of using it:

//Say you have a UIViewController that 
//handles logging in to your app
self.loginViewController = [LoginViewController loginViewController];

//Here we initialize it as the root view controller
//of your main navigation
self.mainNav = [[[TVNavigationController alloc] initWithRootViewController:loginViewController] autorelease];

//Let's say the user interacts with the screen and logs 
//in and now you want to replace the login view controller 
//with the UIViewController that is your application

self.appViewController = [AppViewController appViewController];
//Now we call the new method that let's you set 
//the root view controller 
[mainNav setRootViewController:appViewController];

Below is the code to make all this work:

//// Header file

@interface TVNavigationController : UINavigationController {
  UIViewController *fakeRootViewController;
}

@property(nonatomic, retain) UIViewController *fakeRootViewController;

-(void)setRootViewController:(UIViewController *)rootViewController;

@end
//// Implementation file

#import “TVNavigationController.h”

@implementation TVNavigationController

@synthesize fakeRootViewController;

//override the standard init
-(id)initWithRootViewController:(UIViewController *)rootViewController {
  //create the fake controller and set it as the root
  UIViewController *fakeController = [[[UIViewController alloc] init] autorelease];
  if (self = [super initWithRootViewController:fakeController]) {
    self.fakeRootViewController = fakeController;
    //hide the back button on the perceived root
    rootViewController.navigationItem.hidesBackButton = YES;
    //push the perceived root (at index 1)
    [self pushViewController:rootViewController animated:NO];
  }
  return self;
}

//override to remove fake root controller
-(NSArray *)viewControllers {
    NSArray *viewControllers = [super viewControllers];
	if (viewControllers != nil && viewControllers.count > 0) {
		NSMutableArray *array = [NSMutableArray arrayWithArray:viewControllers];
		[array removeObjectAtIndex:0];
		return array;
	}
	return viewControllers;
}

//override so it pops to the perceived root
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated {
    //we use index 0 because we overrided “viewControllers”
    return [self popToViewController:[self.viewControllers objectAtIndex:0] animated:animated];
}

//this is the new method that lets you set the perceived root, the previous one will be popped (released)
-(void)setRootViewController:(UIViewController *)rootViewController {
    rootViewController.navigationItem.hidesBackButton = YES;
    [self popToViewController:fakeRootViewController animated:NO];
    [self pushViewController:rootViewController animated:NO];
}

- (void)dealloc {
    self.fakeRootViewController = nil;
    [super dealloc];
}

@end
About these ads
Tags:

30 Responses to "Changing a UINavigationController’s Root View Controller"

Great example. But how do I call it / use it / declare it?

A TVNavigationController exactly the same as a normal UINavigationController, but now you have the additional method setRootViewController. To learn more about a UINavigationController see:

http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UINavigationController_Class/Reference/Reference.html

Your example causes NSException error in SDK 3.0

[Session started at 2009-06-23 18:51:25 +0700.]
2009-06-23 18:51:30.534 thedayiwasborn[5881:20b] *** Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘*** -[NSCFArray removeObjectAtIndex:]: index (0) beyond bounds (0)’
2009-06-23 18:51:30.536 thedayiwasborn[5881:20b] Stack: (
807902715,
2456358459,
807986683,
807986522,
810976489,
810598233,
30701,
815256257,
815223834,
815264642,
815242666,
815250384,
30819,
9275,
814713539,
814750709,
814739251,
814722434,
814748641,
839148405,
807687520,
807683624,
814715661,
814752238,
8998,
8870
)

Thanks…I updated the code in the post to work with SDK 3.0 :)

That is one of my fav bugs… : P

Why do you set the fakeRootViewController to nil in the dealloc method rather than just release it like you’d normally do?

In dealloc it’s best to set properties to nil rather than just releasing them…setting a property to nil will do a release and set it’s reference to nil, so there is no chance that it will ever reference an object that isn’t there anymore.

I needed to use something just like this when I was animating a navigation controller using UIViewAnimationTransitionFlipFromLeft and UIViewAnimationTransitionFlipFromRight. When i was calling setRootViewController, the back button was still showing up, so I just added this line to the setRootViewController method.

rootViewController.navigationItem.hidesBackButton = YES;

That seemed to fix the problem.

wanted to post this here in case any one else had the issue.

Good catch…I will update the code to include this line…thanks!

Thank you for this article and code.

I understand the premise of your code, but I can not for the life of me figure out how to implement it.

I humbly petition that you post a sample app or give more information on how its done. PLEASE?

-James

It’s actually pretty straightforward…you use it like a normal UINavigationController, except now you have a new method available to you called setRootViewController.

I will update the post with a simple example.

Cheers!

Brian

Excellent example, I had this problem as well. However, shouldn’t setViewControllers be overridden as well?

Hi very useful tutorial, i am trying to do exactly this but i am unlucky ;(

The first code has to be written in the LoginViewController.m i suppose?

Hi!

I have this code:
[viewController.mainNav setRootViewController:acontrol];

But I’m getting:
warning: no ‘-setRootViewController:’ method found

How’s that? How am I suppose to call that method?

Thanks for your help!

Since I’m loading the navigationcontroller via a nib instead of manually I had to add a call to initWithRootViewController in my viewDidLoad to prevent the switch from popping a view that wasn’t yet there.

After that, it works fine with one caveat:

In my nib I have a navigation button added to my nav item. The swap of the root controller (the push/pop) clears the navigation items in the navbar. How can I easily fake it so that during the pop/push the navigation items are transfered to the pushed controller?

To answer my own question; the behaviour was logical; the navigation item was only tied to one of the rootviews in the nib file. I now set the navigation items up in the viewDidLoad of the baseclass that both rootviewcontrollers derive from; that makes it work.

Hello,

I am very new to xcode and objective c.
I have navigation based application that need to changed tableView to normal view. I think your example is exactly what I need. However, I am a bit confused. what are loginViewController, TVNavigationController. Have you created 2 different UIViewControl.
or which one is root,…
I would be very thankful if you clarify a bit more.

Regards

Thanks for sharing this! Helped save me a bunch of time.

This seems kind of overkill. You can access the viewControllers array like this:

NSMutableArray *views = [[[self navigationController] viewControllers] mutableCopy];

Now that it is mutable, modify the array as you wish. Remove any or all of the view controllers (including the root) and then set it back with:

[[self navigationController] setViewControllers:views];

Thank you!

@interface UINavigationController (UINavigatorController_Extensions)

-(void)changeRootViewController:(UIViewController*)newRoot;

@end

@implementation UINavigationController (UINavigatorController_Extensions)

-(void)changeRootViewController:(UIViewController*)newRoot{

NSMutableArray *controllers;

if ([self.viewControllers count]!=0) {

controllers = [self.viewControllers mutableCopy];
[controllers replaceObjectAtIndex:0 withObject:newRoot];
}
else{
controllers=[NSMutableArray arrayWithObject:newRoot];
}

self.viewControllers=controllers;
}

@end

controllers = [self.viewControllers mutableCopy]; is leaking..

Perfect answer..

It seems pretty expensive to re-compute the viewControllers array in popToRootViewControllerAnimated, why not just get the objectAtIndex:1 instead?

A sample code project would really have helped.

There’s only one problem in that code. The case of using it from nib file (let’s say MainWindow.xib). In such scenario the method initWithRootViewController does not get called. I’v added the following to solve the issue:

-(id)initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if(self){
UIViewController *fakeController = [[[UIViewController alloc] init] autorelease];
self.fakeRootViewController = fakeController;
NSMutableArray *array = [NSMutableArray arrayWithArray:[super viewControllers]];
[array insertObject:fakeController atIndex:0];
self.viewControllers = array;
}
return self;
}

Why not just do something like this:

myNavController.viewControllers := [NSArray arrayWithObjects: myNewRootController, nil];

Should be equivalent to changing the root controller…

In the popToRootViewController, shouldn’t it pop to index 1, where the real root view controller is, instead of index 0 where the fake view controller is?

Awesome.. Thanks man save me a ton of time and effort.

You forgot to release the view controller after you set it to root view controller in your simple example .

To change a rootViewController, without all this TVNavigationController :

myNewRoot = [[UIViewController alloc] init];
myNavigationController.viewControllers = @[myNewRoot];

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


  • Dave: I can tell you're a ruby guy because you forgot the 'return' keyword. Thanks for the tip though!
  • Chandrashekhar H M: Hi, Thanks its working fine in iOS 6 but not in iOS 7.0. Any Suggestion on this.
  • Coeur: To change a rootViewController, without all this TVNavigationController : myNewRoot = [[UIViewController alloc] init]; myNavigationController.view
Follow

Get every new post delivered to your Inbox.

%d bloggers like this: