84

I have an application where I need to remove one view from the stack of a UINavigationController and replace it with another. The situation is that the first view creates an editable item and then replaces itself with an editor for the item. When I do the obvious solution within the first view:

MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

[self retain];
[self.navigationController popViewControllerAnimated: NO];
[self.navigationController pushViewController: mevc animated: YES];
[self release];

I get very strange behavior. Usually the editor view will appear, but if I try to use the back button on the nav bar I get extra screens, some blank, and some just screwed up. The title becomes random too. It is like the nav stack is completely hosed.

What would be a better approach to this problem?

Thanks, Matt

Shekhar Gupta
  • 6,206
  • 3
  • 30
  • 50
Matt Brandt
  • 901
  • 1
  • 7
  • 5

16 Answers16

137

I've discovered you don't need to manually mess with the viewControllers property at all. Basically there are 2 tricky things about this.

  1. self.navigationController will return nil if self is not currently on the navigation controller's stack. So save it to a local variable before you lose access to it.
  2. You must retain (and properly release) self or the object who owns the method you are in will be deallocated, causing strangeness.

Once you do that prep, then just pop and push as normal. This code will instantly replace the top controller with another.

// locally store the navigation controller since
// self.navigationController will be nil once we are popped
UINavigationController *navController = self.navigationController;

// retain ourselves so that the controller will still exist once it's popped off
[[self retain] autorelease];

// Pop this controller and replace with another
[navController popViewControllerAnimated:NO];
[navController pushViewController:someViewController animated:NO];

In that last line if you change the animated to YES, then the new screen will actually animate in and the controller you just popped will animate out. Looks pretty nice!

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • brilliant! much better solution – emmby Feb 26 '10 at 02:15
  • Awesome. Although I didn't need to call [[self retain] autorelease], it still works fine. – iamj4de Jun 18 '10 at 15:59
  • Interesting, my problem seemed to be caused by having the popViewControllerAnimated set to yes. I set it to know and left the push as yes and it looks just fine! Cheers. – toxaq Sep 27 '10 at 14:05
  • 4
    Perhaps an obvious addition, but you can then put the code above in a animation block to animate the transition: [UIView beginAnimations:@"View Flip" context:nil]; [UIView setAnimationDuration:0.80]; [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut]; [UIView setAnimationTransition: UIViewAnimationTransitionFlipFromRight forView:navController.view cache:NO]; [navController pushViewController:newController animated:YES]; [UIView commitAnimations]; – Martin Jun 29 '11 at 12:17
  • 11
    Works great with ARC just by removing the retain/autorelease line. – Ian Terrell Mar 21 '12 at 21:35
  • The solution works, except when the UIViewController in question is on top of the stack: than `[navController popViewControllerAnimated:NO]` doesn't work (Apple documentation also states this), and you get two VCs on the stack. I've found that you can call `[navController setViewControllers:@[yourViewController]]` in that case, and everything should work smoothly. – nikolovski Feb 04 '13 at 10:24
  • @MarkoNikolovski, it works for me (iOS 6.1). But maybe you mean on the **bottom** of the stack (i.e., it's the root vc)? If so, you just need to avoid putting this code in the root ViewController. A completely alternative solution would work explicitly with the stack, as seen [here](http://stackoverflow.com/a/17328244/1431728). – JohnK Jun 26 '13 at 19:17
  • @JohnK, yes, that's correct, my bad. I was basically had in mind the case when there's only one VC, so I didn't think about the top/bottom semantics :) – nikolovski Jun 27 '13 at 09:53
  • 2
    @TomerPeled Yeah this answer is nearly 5 years old... I think that was the case in like iOS 3. The APIs have changed enough that I'm not sure it's the best answer anymore. – Alex Wayne Aug 18 '14 at 17:44
  • Actually for now the solution which @Luke Rogers offered below works good. – Tomer Peled Aug 18 '14 at 17:46
56

The following approach seems nicer to me, and also works well with ARC:

UIViewController *newVC = [[UIViewController alloc] init];
// Replace the current view controller
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[[self navigationController] viewControllers]];
[viewControllers removeLastObject];
[viewControllers addObject:newVC];
[[self navigationController] setViewControllers:viewControllers animated:YES];
Luke Rogers
  • 2,369
  • 21
  • 28
  • 1
    @LukeRogers, this causes the following warning for me: Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted. Any way to suppress it? – zaitsman Aug 18 '13 at 06:45
  • Using this solution, you overwrite the popover. And to show in the DetailView, your code should read: `if(indexPath.row == 0){UIViewController *newVC = [[UIViewController alloc] init];newVC = [self.storyboard instantiateViewControllerWithIdentifier:@"Item1VC"]; NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[_detailViewController.navigationController viewControllers]]; [viewControllers removeLastObject];[viewControllers addObject:newVC]; [_detailViewController.navigationController setViewControllers:viewControllers animated:YES];}` – LAOMUSIC ARTS Nov 08 '13 at 12:52
9

From experience, you're going to have to fiddle with the UINavigationController's viewControllers property directly. Something like this should work:

MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

[[self retain] autorelease];
NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];
[controllers removeLastObject];
self.navigationController.viewControllers = controllers;
[self.navigationController pushViewController:mevc animated: YES];

Note: I changed the retain/release to a retain/autorelease as that's just generally more robust - if an exception occurs between the retain/release you'll leak self, but autorelease takes care of that.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
7

After much effort (and tweaking the code from Kevin), I finally figured out how to do this in the view controller that is being popped from the stack. The problem that I was having was that self.navigationController was returning nil after I removed the last object from the controllers array. I think it was due to this line in the documentation for UIViewController on the instance method navigationController "Only returns a navigation controller if the view controller is in its stack."

I think that once the current view controller is removed from the stack, its navigationController method will return nil.

Here is the adjusted code that works:

UINavigationController *navController = self.navigationController;
MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];
[controllers removeLastObject];
navController.viewControllers = controllers;
[navController pushViewController:mevc animated: YES];
4

Thanks, this was exactly what I needed. I also put this in an animation to get the page curl:

        MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

    UINavigationController *navController = self.navigationController;      
    [[self retain] autorelease];

    [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration: 0.7];
    [UIView setAnimationTransition:<#UIViewAnimationTransitionCurlDown#> forView:navController.view cache:NO];

    [navController popViewControllerAnimated:NO];
    [navController pushViewController:mevc animated:NO];

    [UIView commitAnimations];

0.6 duration is fast, good for 3GS and newer, 0.8 is still a bit too fast for 3G..

Johan

Johan
  • 41
  • 1
  • Your code is exactly what I used, great! Thanks. One note: with page curl transition I got a white artifice at bottom of view (who knows why) but with flip it worked fine. Anyway, this is nice and compact code! – David H Dec 02 '11 at 21:40
3

If you want to show any other view controller by popToRootViewController then you need to do following:

         UIViewController *newVC = [[WelcomeScreenVC alloc] initWithNibName:@"WelcomeScreenVC" bundle:[NSBundle mainBundle]];
            NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[[self navigationController] viewControllers]];
            [viewControllers removeAllObjects];
            [viewControllers addObject:newVC];
            [[self navigationController] setViewControllers:viewControllers animated:NO];

Now, all your previous stack will be removed and new stack will be created with your required rootViewController.

msmq
  • 1,298
  • 16
  • 28
1

Here is another approach that doesn't require directly messing with the viewControllers array. Check if the controller has been pop'd yet, if so push it.

TasksViewController *taskViewController = [[TasksViewController alloc] initWithNibName:nil bundle:nil];

if ([navigationController.viewControllers indexOfObject:taskViewController] == NSNotFound)
{
    [navigationController pushViewController:taskViewController animated:animated];
}
else
{
    [navigationController popToViewController:taskViewController animated:animated];
}
ezekielDFM
  • 1,947
  • 13
  • 26
1
NSMutableArray *controllers = [self.navigationController.viewControllers mutableCopy];
    for(int i=0;i<controllers.count;i++){
       [controllers removeLastObject];
    }
 self.navigationController.viewControllers = controllers;
Alessandro Minoccheri
  • 35,521
  • 22
  • 122
  • 171
Ravi
  • 11
  • 1
  • this causes a warning for me in the console - Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted. Any way to suppress it? – zaitsman Aug 18 '13 at 06:44
1

My favorite way to do it is with a category on UINavigationController. The following should work:

UINavigationController+Helpers.h #import

@interface UINavigationController (Helpers)

- (UIViewController*) replaceTopViewControllerWithViewController: (UIViewController*) controller;

@end

UINavigationController+Helpers.m
#import "UINavigationController+Helpers.h"

@implementation UINavigationController (Helpers)

- (UIViewController*) replaceTopViewControllerWithViewController: (UIViewController*) controller {
    UIViewController* topController = self.viewControllers.lastObject;
    [[topController retain] autorelease];
    UIViewController* poppedViewController = [self popViewControllerAnimated:NO];
    [self pushViewController:controller animated:NO];
    return poppedViewController;
}

@end

Then from your view controller, you can replace the top view with a new by like this:

[self.navigationController replaceTopViewControllerWithViewController: newController];
bbrame
  • 18,031
  • 10
  • 35
  • 52
1

This UINavigationController instance method might work...

Pops view controllers until the specified view controller is the top view controller and then updates the display.

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
Maulik
  • 19,348
  • 14
  • 82
  • 137
diclophis
  • 2,444
  • 1
  • 17
  • 10
1

I had to do a similar thing recently and based my solution on Michaels answer. In my case I had to remove two View Controllers from the Navigation Stack and then add a new View Controller on. Calling

[controllers removeLastObject];
twice, worked fine in my case.

UINavigationController *navController = self.navigationController;

// retain ourselves so that the controller will still exist once it's popped off
[[self retain] autorelease];

searchViewController = [[SearchViewController alloc] init];    
NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];

[controllers removeLastObject];
// In my case I want to go up two, then push one..
[controllers removeLastObject];
navController.viewControllers = controllers;

NSLog(@"controllers: %@",controllers);
controllers = nil;

[navController pushViewController:searchViewController animated: NO];

mattlangtree
  • 417
  • 2
  • 9
0

You can check with navigation view controllers array which you give you all view controllers that you have added in navigation stack. By using that array you can back navigate to specific view controller.

jignesh
  • 41
  • 1
0

For monotouch / xamarin IOS:

inside UISplitViewController class;

UINavigationController mainNav = this._navController; 
//List<UIViewController> controllers = mainNav.ViewControllers.ToList();
mainNav.ViewControllers = new UIViewController[] { }; 
mainNav.PushViewController(detail, true);//to have the animation
Nabil.A
  • 581
  • 4
  • 16
0

Alternatively,

You can use category to avoid self.navigationController to be nil after popViewControllerAnimated

just pop and push, it's easy to understand, don't need to access viewControllers....

// UINavigationController+Helper.h
@interface UINavigationController (Helper)

- (UIViewController*) popThenPushViewController:(UIViewController *)viewController animated:(BOOL)animated;

@end


// UINavigationController+Helper.m
@implementation UINavigationController (Helper)

- (UIViewController*) popThenPushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    UIViewController *v =[self popViewControllerAnimated:NO];

    [self pushViewController:viewController animated:animated];

    return v;
}
@end

In your ViewController

// #import "UINavigationController+Helper.h"
// invoke in your code
UIViewController *v= [[MyNewViewController alloc] init];

[self.navigationController popThenPushViewController:v animated:YES];

RELEASE_SAFELY(v);
payliu
  • 2,410
  • 3
  • 18
  • 12
0

Not exactly the answer but might be of help in some scenarios (mine for example):

If you need to pop viewcontroller C and go to B (out of stack) instead of A (the one bellow C), it's possible to push B before C, and have all 3 on the stack. By keeping the B push invisible, and by choosing whether to pop only C or C and B altogether, you can achieve the same effect.

initial problem A -> C (I want to pop C and show B, out of stack)

possible solution A -> B (pushed invisible) -> C (when I pop C, I choose to show B or also pop it)

alasker
  • 309
  • 5
  • 18
0

I use this solution to keep the animation.

[self.navigationController pushViewController:controller animated:YES];
NSMutableArray *newControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
[newControllers removeObject:newControllers[newControllers.count - 2]];
[self.navigationController setViewControllers:newControllers];
code4j
  • 4,208
  • 5
  • 34
  • 51