Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pushing view controller within viewDidAppear doesn't work

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!

like image 965
Brenden Avatar asked Jan 28 '14 21:01

Brenden


People also ask

How do I push a view controller?

Pushing a view controller causes its view to be embedded in the navigation interface. If the animated parameter is true , the view is animated into position; otherwise, the view is simply displayed in its final location.

How do I add a view controller to my navigation controller?

Step 1: Embed root view controller inside a navigation controller. In your storyboard, select the initial view controller in your hierarchy. With this view controller selected, choose the menu item Editor -> Embed In -> Navigation Controller .

What is a UIViewController?

The UIViewController class defines the shared behavior that's common to all view controllers. You rarely create instances of the UIViewController class directly. Instead, you subclass UIViewController and add the methods and properties needed to manage the view controller's view hierarchy.

Should I call super viewDidAppear?

Not calling [super viewDidAppear] is indeed a bad idea. The base class for UIViewController has code in viewDidAppear that needs to be called or things won't work correctly.


1 Answers

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.

like image 137
stefandouganhyde Avatar answered Nov 10 '22 11:11

stefandouganhyde