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
24 Responses to "Changing a UINavigationController’s Root View Controller"
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
)
Why do you set the fakeRootViewController to nil in the dealloc method rather than just release it like you’d normally do?
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.
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
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
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
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;
}
March 29, 2009 at 5:17 pm
Great example. But how do I call it / use it / declare it?