Dima Dima - 1 month ago 22
iOS Question

Canceling interactive UINavigationController pop gesture does not call UINavigationControllerDelegate methods

If you drag around a

UIViewController
to begin an interactive pop transition within a
UINavigationController
, the
UIViewController
underneath the current one has
viewWillAppear:
called, followed by the
UINavigationControllerDelegate
method
navigationController:willShowViewController:animated:


If you cancel the transition, both
viewWillAppear:
and
viewDidAppear:
get called on the top view controller as expected.

However, neither of the delegate methods
navigationController:willShowViewController:animated:
or
navigationController:didShowViewController:animated:
are called at all.

It seems like at least one or both of these should be called considering the UIViewController view lifecycle methods are called. I am wondering whether this is deliberate or a bug in
UINavigationController
.

What I really need is to be able to see when an interactive pop is cancelled, either within my
UINavigationController
subclass, or its
UINavigationControllerDelegate
. Is there an obvious way to do this?

edit

I'm still looking for a solution to this but would like to mention that I have reported this issue as a bug with Apple. Looking at the documentation, there is no reason these delegate methods should not get called, especially considering the equivalent view lifecycle methods DO get called.

edit2

My radar ticket (16823313) was closed today (May 21st, 2015) and marked as intended. :(


Engineering has determined that this issue behaves as intended based
on the following information:

This is actually the correct behavior. The navigation transition
that's happening from B -> A, if you cancel it mid-transition, you
won't get the didShowViewController: method. A cancellation of this
transition shouldn't be considered a transition from A -> B because
you never actually reached A.

view[Will/Did]Appear should still be called as expected too.


Quite a bummer this is the case as it is counterintuitive but the workaround in my answer below should work fine for the foreseeable future, at least for my use-case.

Answer

For anyone interested, I have found 2 ways to work around this at the UINavigationControllerDelegate level.

  1. Use KVO to observe the state property of the interactivePopGestureRecognizer. Unfortunately, canceling the transition does not change the state to UIGestureRecognizerStateFailed but instead just UIGestureRecognizerStateEnded, so you would need to write a bit of additional code to keep track of what happened if you needed to discern between a cancelled or completed pop.

  2. After testing it, this is probably the better solution: Use the navigationController:willShowViewController:animated: method to add a notification block to the transition coordinator. It looks something like this:

    - (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
    {
        [[self transitionCoordinator] notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context)
        {
            if([context isCancelled])
            {
                UIViewController *fromViewController = [context viewControllerForKey:UITransitionContextFromViewControllerKey];
                [self navigationController:navigationController willShowViewController:fromViewController animated:animated];
    
                if([self respondsToSelector:@selector(navigationController:didShowViewController:animated:)])
                {
                    NSTimeInterval animationCompletion = [context transitionDuration] * [context percentComplete];
    
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (uint64_t)animationCompletion * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
                        [self navigationController:navigationController didShowViewController:fromViewController animated:animated];
                    });
                }
    
    
            }
        }];
    }
    

I was hesitant to use this solution at first because the documentation was unclear about whether or not you could set more than one of these (since in that case, if an unknowing view controller also set its own notification block, it could potentially either replace this one or get replaced by this one). After testing it though, it appears that it is not a 1:1 relationship and you can add multiple notification blocks safely.

edit

I edited the code above to delay the navigationController:didShowViewController:animated: call to only be called when the animation is supposed to be completed to more closely match the expected default behavior.

Comments