36

Is there a clean solution on getting a callback or event on the view controller being dismissed (popped) by an interactivePopGestureRecognizer?

To be clear I need some explicit method getting called on the top most controller (and no other) before the controller will be popped by this gesture recogniser. I do not want to get the event on the navigation controller and send the event to the appropriate controller and I do not want to use viewWillAppear or viewWillDissapear...

The closest thing I have is adding a target/selector pair to the gesture having only 2 problems. First I can't get a direct information if the controller will be dismissed or not (UIGestureRecognizerStateEnded will fire in any case). Second after the controller is dismissed I need to remove the target from the recogniser.

The reason for this is I have a few controllers that need to send some information to their delegates. With having "done" and "cancel" buttons the event is triggered, delegate methods get called and then the controller is popped. I need pretty much the same to happen with as least changes to the code as possible.

Another situation on this gesture is possibility of throwing an alert view and reverting the action: Is there a way of showing alert view when this gesture ends asking like "are you sure you wish to cancel your work" and have the user choose if the controller will be popped or brought back.

Matic Oblak
  • 16,318
  • 3
  • 24
  • 43
  • Interesting problem. I have a feeling you'll need to disable the `interactivePopGestureRecognizer` and register your own or use iOS 7's [interactive transitions](http://www.objc.io/issue-5/view-controller-transitions.html) – David Snabel-Caunt Dec 17 '13 at 16:58
  • I am afraid you might be right. Seems strange this isn't a common problem. I would expect quite a few apps needing to implement this at least at some point. The worst thing about implementing a custom transition is you have to then explicitly disable the gesture on each of those controllers and reenable it after it is popped (or another one is pushed). – Matic Oblak Dec 18 '13 at 08:25

4 Answers4

66

I know this is old but for anyone else who might be facing similar problems. Here is the approach I used. First I register a UINavigationControllerDelegate to my navigation controller. The delegate needs to implement.

Objective-C

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated

Swift

func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool)

So the implementation would look something like this.

Objective-C

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
        id<UIViewControllerTransitionCoordinator> tc = navigationController.topViewController.transitionCoordinator;
        [tc notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
            NSLog(@"Is cancelled: %i", [context isCancelled]);
    }];
}

Swift

func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
    if let coordinator = navigationController.topViewController?.transitionCoordinator() {
        coordinator.notifyWhenInteractionEndsUsingBlock({ (context) in
            print("Is cancelled: \(context.isCancelled())")
        })
    }
}

The callback will fire when the user lifts her finger and the (Objec-C)[context isCancelled]; (Swift)context.isCancelled() will return YES/true if the animation was reversed (the view controller was not popped), else NO/false. There is more stuff in the context that can be of use, like both view controllers involved and the percentage of the animation that was completed upon release etc.

Peter Segerblom
  • 2,773
  • 1
  • 19
  • 24
  • 1
    It doesn't fit the "I do not want to get the event on the navigation controller and send the event to the appropriate controller" but still quite good. – Matic Oblak Jan 16 '14 at 07:50
  • 1
    Yes i know it's not perfect for you but your question is one of the first if you try to google this type of issue so it might work for some. And technically you are not getting this on the navigation controller but on the delegate, but yes you still need to call a method on the controller. If you want to give it a try you can use the isCancelled to check if you want to notify or not and then use the [context viewControllerForKey:UITransitionContextFromViewControllerKey] to get the get the controller to notify. But again you are right it's not what you aced for. – Peter Segerblom Jan 16 '14 at 08:01
  • 1
    Actually by having the top most controller I can implement something like this: if([tc respondsToSelector:@selector(controllerDismissedByPopGesture:)]) { [tc performSelector:@selector(controllerDismissedByPopGesture:) withObject:@([context isCancelled])]; } Although I don't like this type of code very much it does exactly what I need. All I need is to add the `- (void)controllerDismissedByPopGesture:(NSNumber *)isCancelled` to ANY controller and I'm done. Maybe you should add it to your answer... – Matic Oblak Jan 16 '14 at 08:22
  • 3
    I had to do some modifications... id tc = navigationController.topViewController.transitionCoordinator; [tc notifyWhenInteractionEndsUsingBlock:^(id context) { UIViewController *topController = [context viewControllerForKey:UITransitionContextFromViewControllerKey]; if([topController respondsToSelector:@selector(controllerDismissedByPopGesture:)]) { [topController performSelector:@selector(controllerDismissedByPopGesture:) withObject:@([context isCancelled])]; } }]; – Matic Oblak Jan 16 '14 at 08:34
22

Swift 4 iOS 7 - 10

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {

    if let coordinator = navigationController.topViewController?.transitionCoordinator {
        coordinator.notifyWhenInteractionEnds({ (context) in
            print("Is cancelled: \(context.isCancelled)")
        })
    }
}

Swift 4 - 5.1 iOS 10+

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {

    if let coordinator = navigationController.topViewController?.transitionCoordinator {
        coordinator.notifyWhenInteractionChanges { (context) in
            print("Is cancelled: \(context.isCancelled)")
        }
    }
}
Ted
  • 22,696
  • 11
  • 95
  • 109
1

I know that original question asked not to use viewWillDisappear but there is a property isMovingFromParent which helps to distinguish desired logic from the rest of the other code that can be presented in this method. And we don't need navigationController delegate in this case:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    guard isMovingFromParent else {
        print("We are not going back at this moment")
        return
    }

    transitionCoordinator?.animate(alongsideTransition: { context in
        if context.isInteractive {
            print("Interactive swipe transition. Start.")
        } else {
            print("Back button transition. Start.")
        }
    }, completion: { context in
        if context.isCancelled {
            print("Interactive swipe transition. Finish. Cancelled. We are still on child screen.")
        } else if context.initiallyInteractive {
            print("Interactive swipe transition. Finish. Sucess. We are on parent screen.")
        } else {
            print("Back button transition. Finish. Sucess. We are on parent screen.")
        }
    })

    transitionCoordinator?.notifyWhenInteractionChanges { _ in
        print("Interactive swipe transition. Finger lifted up or moved back to edge.")
    }
}
nemissm
  • 453
  • 6
  • 12
-1
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {

    void (^popInteractionEndBlock)(id<UIViewControllerTransitionCoordinatorContext>) =
    ^(id<UIViewControllerTransitionCoordinatorContext> context){
        if (context.completionVelocity >= 1.0f) {
            // complete
        }
    };
    if (@available(iOS 10,*)) {
        [self.navigationController.transitionCoordinator notifyWhenInteractionChangesUsingBlock:popInteractionEndBlock];
    } else {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
        [self.navigationController.transitionCoordinator notifyWhenInteractionEndsUsingBlock:popInteractionEndBlock];
#pragma GCC diagnostic pop
    }
}

WFour
  • 1