How can I pop a view from a UINavigationController and replace it with another in one operation?

92

Solution 1

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!

Solution 2

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];

Solution 3

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.

Solution 4

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];

Solution 5

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

Share:
92

Related videos on Youtube

joshmcode
Author by

joshmcode

Updated on February 27, 2020

Comments

  • joshmcode
    joshmcode over 4 years

    If I had a method like below:

    public Person GetPerson(int id)
    {
      // code that returns a Person where id is == id
    }
    

    Let's also so that Person looks like this:

    public class Person 
    {
       public int id {get; set;}
       public string firstName {get; set;}
       public string lastName {get;set}
    }
    

    I know that I could call it like so:

    string name = GetPerson(someId).firstName;
    

    What is the syntax where I could chain the firstName and lastName together? I am aware that some of the answers might be, "just create a field fullName" but I am actually just trying to expand my knowledge of C# to see if there is a way to chain those fields together WITHOUT doing this:

    string name = $"{GetPerson(someId).firstName} {GetPerson(someId).lastName}"; 
    

    Also, I don't want to return the Person object and then combine them. I'd like to (if it is possible) just combine it all in one line with chaining. I've tried several ways and can't get it to compile.

    Edit What original prompted me to want to do this was to avoid calling the GetPerson method 2x when using a concatenation approach to getting the full name (when not just creating a variable for this purpose). As was pointed out in the comments by Servy, this is really what variables are for and, as was also pointed out by O. R. Mapper, what I was looking for does not appear to be supported by C# at this time.

    • Servy
      Servy over 7 years
      Store the person in a variable. This is exactly what they exist for.
    • Admin
      Admin over 7 years
      What do you mean by "chain"?
    • ColinM
      ColinM over 7 years
      Why not just add a full name property on the object that uses string interpolation or string.Format to join the full name?
    • ColinM
      ColinM over 7 years
      Alright, ignore the first part of my comment - after seeing your message regarding the fullName property, but now my question is why are you trying to "expand knowledge" by adding complication? Even the most experienced developers would use a property, otherwise you're adding complication for the sake of adding complication.
    • joshmcode
      joshmcode over 7 years
      @DStanley Nothing at all. As my question states, its just a language question and not a proper-way-to-do-something or practicality question. No panda bears will be saved or lost by this question being answered :-)
    • trashr0x
      trashr0x over 7 years
      Further to the comments, OP could add "in X language, I would do this: showUsHowYouWouldDoIt();" to his post, so it becomes more clear what he's after.
    • O. R. Mapper
      O. R. Mapper over 7 years
      @trashr0x: As far as I understood, the OP is looking for some syntax such as string name = with GetPerson(someId) { firstName + " " + lastName } (which would then return the expression in the braces, with identifiers evaluated in the scope GetPerson(someId)). I do not think anything to that effect is currently supported by C#, though.
  • emmby
    emmby over 14 years
    brilliant! much better solution
  • iamj4de
    iamj4de about 14 years
    Awesome. Although I didn't need to call [[self retain] autorelease], it still works fine.
  • toxaq
    toxaq over 13 years
    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.
  • Martin
    Martin almost 13 years
    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];
  • David H
    David H over 12 years
    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!
  • Ian Terrell
    Ian Terrell over 12 years
    Works great with ARC just by removing the retain/autorelease line.
  • nikolovski
    nikolovski over 11 years
    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.
  • JohnK
    JohnK almost 11 years
    @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.
  • nikolovski
    nikolovski almost 11 years
    @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 :)
  • zaitsman
    zaitsman almost 11 years
    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
    zaitsman almost 11 years
    @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?
  • LAOMUSIC ARTS
    LAOMUSIC ARTS over 10 years
    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
    LAOMUSIC ARTS over 10 years
    This gives me a black whole !
  • JERC
    JERC almost 10 years
    What I was looking for.
  • Alex Wayne
    Alex Wayne almost 10 years
    @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.
  • Tomer Peled
    Tomer Peled almost 10 years
    Actually for now the solution which @Luke Rogers offered below works good.
  • Servy
    Servy over 7 years
    Now you're getting the person twice, rather than once. Also, this isn't really any different than the OP's code, and has all of the same problems.
  • Servy
    Servy over 7 years
    But this is explicitly declaring variables, the variables are just parameters to a method instead of a local variable, but they're both variables.
  • Servy
    Servy over 7 years
    But you do. p. It's a variable of type Person, that you've declared.
  • Evk
    Evk over 7 years
    Well anyway author just wants to explore options, and this is one option. However I removed a note about explicit variables.
  • trashr0x
    trashr0x over 7 years
    Read the question.