14

So I have a UINavigationController, with Controller A as the root controller.

When I want to push Controller B on top, I want to use a custom animated transition and a custom interactive transition. This works fine.

When I want to push Controller C on top, I want to fall back to the default push/pop transitions that comes with UINavigationController. To make this happen I return nil for

navigationController:animationControllerForOperation:fromViewController:toViewController:

however if you return nil, then

navigationController:interactionControllerForAnimationController:

will never be called and the default "pan from left edge" pop interactive transition doesn't work.

Is there a way to return the default push/pop animation controller and interaction controller? (Are there concrete implementations of id<UIViewControllerAnimatedTransitioning> and id<UIViewControllerInteractiveTransitioning>?)

Or some other way?

just.jimmy
  • 954
  • 1
  • 8
  • 15

5 Answers5

19

You should set NavigationController's interactivePopGestureRecognizer delegate to self and then handle its behavior in -gestureRecognizerShouldBegin:

That is, when you want the built-in pop gesture to fire, you must return YES from this method. The same goes for your custom gestures - you have to figure out which recognizer you are dealing with.

- (void)setup
{
    self.interactiveGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTransitionGesture:)];
    self.interactiveGestureRecognizer.delegate = self;
    [self.navigationController.view addGestureRecognizer:self.interactiveGestureRecognizer];

    self.navigationController.interactivePopGestureRecognizer.delegate = self;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    // Don't handle gestures if navigation controller is still animating a transition
    if ([self.navigationController.transitionCoordinator isAnimated])
        return NO;

    if (self.navigationController.viewControllers.count < 2)
        return NO;

    UIViewController *fromVC = self.navigationController.viewControllers[self.navigationController.viewControllers.count-1];
    UIViewController *toVC = self.navigationController.viewControllers[self.navigationController.viewControllers.count-2];

    if ([fromVC isKindOfClass:[ViewControllerB class]] && [toVC isKindOfClass:[ViewControllerA class]])
    {
        if (gestureRecognizer == self.interactiveGestureRecognizer)
            return YES;
    }
    else if (gestureRecognizer == self.navigationController.interactivePopGestureRecognizer)
    {
        return YES;
    }

    return NO;
}

You can check out a sample project for your scenario. Transitions between view controllers A and B are custom animated, with custom B->A pop gesture. Transitions between view controllers B and C are default, with built-in navigation controller's pop gesture.

Hope this helps!

maxkonovalov
  • 3,651
  • 34
  • 36
  • 2
    According to the DTS folks at Apple, the private interactivePopGestureRecognizer that ships with UINavigationController does a number of checks in its delegate, some of which call into private API. Implementing your own delegate isn't guaranteed to cover 100% of all cases. – Ziconic Jan 17 '14 at 23:50
  • @Ziconic example case? – pronebird Dec 16 '15 at 13:15
15

You'll need to set the delegate every time before you present - in prepareForSeque: for example. If you want the custom transition, set it to self. If you want the default transition (like the default pop transition) set it to nil.

Jesse
  • 1,667
  • 12
  • 16
  • 4
    Sadly this is actually the correct answer and the approach that the Apple DTS folks recommend, even though it is not a particularly elegant one. Basically you need to have 2 UINavigationControllerDelegate objects, one that implements the transition callbacks and one that does not (or is nil). To get the default pop transition, use the delegate without the callbacks (or set the delegate to nil). To get the custom transition, swap out the delegate for the one that has the callbacks (and restore the original delegate after the transition completes). – Ziconic Jan 17 '14 at 23:54
  • 2
    Is this still the recommended approach? – ken Nov 16 '14 at 02:43
1

Setting the delegate before/after each transition is a valid workaround, but if you implement other UINavigationControllerDelegate's methods and you need to keep them, you can either have 2 delegate objects as suggested by Ziconic or play with NSObject's respondsToSelector:. In your navigation delegate you could implement:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if (aSelector == @selector(navigationController:animationControllerForOperation:fromViewController:toViewController:) ||
        aSelector == @selector(navigationController:interactionControllerForAnimationController:)) {

        return self.interactivePushTransitionEnabled;
    }

    return [super respondsToSelector:aSelector];
}

You should then make sure to update interactivePushTransitionEnabled as needed. In your example, you should set the property to YES only when controller A is being displayed.

There is only one more thing to do: force UINavigationController to reevaluate the methods implemented by its delegate. This can be easily done by doing something like this:

navigationController.delegate = nil;
navigationController.delegate = self; // or whatever object you use as the delegate
Xavier Jurado
  • 61
  • 1
  • 3
0

try setting (self.)navigationController.delegate to nil (or return it to a previous value) after you've done your custom transition.

ScottJShea
  • 7,041
  • 11
  • 44
  • 67
glotcha
  • 558
  • 6
  • 13
0

in your gesture recognizer, set the delegate (to yourself) there and return your animated and interactive transitions. when returning the interactive transition, be sure to unset the delegate again so that everything else continues to use the default transitions.

I have a working example on github.

- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
{
  // Unset the delegate so that all other types of transitions (back, normal animated push not initiated by a gesture) get the default behavior.
  self.navigationController.delegate = nil;

  if (self.edgePanGestureRecognizer.state == UIGestureRecognizerStateBegan) {
    self.percentDrivenInteractiveTransition = [UIPercentDrivenInteractiveTransition new];
  } else {
    self.percentDrivenInteractiveTransition = nil;
  }

  return self.percentDrivenInteractiveTransition;
}
visnup
  • 499
  • 4
  • 5