Pushing view controller within viewDidAppear doesn't work

ios, uinavigationcontroller

Solution

This is an interesting question. I'm pretty confident that if you were to set `animated:YES` when pushing the second view controller in firstViewController.m, the final UI state would look as expected, with the third screen's content and title both correctly visible.

However, this is clearly not the transition effect you're aiming for. And why does the `animated` flag make an iota of difference anyway?

If you set a break point in `-viewDidAppear:` and look at the stack traces both for the case where `animated == YES` and `animated == NO`, it looks to me like when `animated == NO`, `-viewDidAppear:` is invoked during a view layout operation in the `UINavigationController`. My money's on this being the reason that you're final view looks incorrect; performing a push now would be doing so before the previous push has completely finished.

This is where run loop considerations come in. We want the `UINavigationController`'s view layout (which is happening on the main run loop's current loop cycle) to finish before asking for the next push. A simple way to achieve that is to queue the push to happen on the next cycle of the main run loop. A delay will certainly do the trick (I believe a delay of `0` is enough to delay to the next run loop cycle, so you could try replacing `250 * USEC_PER_SEC` with `0`). Another way would be to dispatch the action onto the main queue:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.navigationController pushViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"thirdScreen"] animated:YES];
});

So my answer is kind of speculative, but based on some evidence. It feels slightly unsatisfactory that, when performing `UINavigationController` transitions, `-viewDidAppear:` only indicates the true end of the transition when it's animated, however that does seem to be the case.

Problem

Steps to Reproduce 1) Create a navigation controller and 3 view controllers. firstViewController.m: ``` - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"DEBUG: first screen did appear"); [self.navigationController pushViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"secondScreen"] animated:NO]; } ``` secondViewController.m: ``` - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"DEBUG: second screen did appear"); [self.navigationController pushViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"thirdScreen"] animated:YES]; } ``` thirdViewController.m: ``` - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"DEBUG: third screen did appear"); } ``` 2) make `firstViewController` (aka firstScreen in storyboard) the root view controller of the navigation controller. 3) Run app and notice that the navigation bar has updated to show the third screen's title, but still shows the second screen's content. Notes I've tried using `UINavigationControllerDelegate`'s `-( void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated` method since it seems to fire after the `viewDidAppear` method, but it didn't fix the issue. I also tried manually setting the navigation controller's `viewControllers` thinking it would skip some "this view controller is active" logic and allow the problematic push to work, but it didn't. Solution The only solution I could come up with was using a delayed call to push the desired view controller in secondViewController.m: ``` dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 250 * USEC_PER_SEC), dispatch_get_main_queue(), ^{ [self.navigationController pushViewController:[self.storyboard instantiateViewControllerWithIdentifier:@"thirdScreen"] animated:YES]; }); ``` Problem I'd like to understand why this is not working as expected. Based on some other SO answers I've seen on semi-relevant questions, it may have something to do with a run loop, but I can't confirm or deny that (seems possible since dispatching the push allows it to work). Can anyone else with more knowledge/experience enlighten me? Thanks!

Original source