Navigation pop view when swipe right like Instagram iPhone app.How i achieve this?

13,615

Solution 1

Apple's automatic implementation of the "swipe right to pop VC" only works for the left ~20 points of the screen. This way, they make sure they don't mess with your app's functionalities. Imagine you have a UIScrollView on screen, and you can't swipe right because it keeps poping VCs out. This wouldn't be nice.

Apple says here :

interactivePopGestureRecognizer

The gesture recognizer responsible for popping the top view controller off the navigation stack. (read-only)

@property(nonatomic, readonly) UIGestureRecognizer *interactivePopGestureRecognizer

The navigation controller installs this gesture recognizer on its view and uses it to pop the topmost view controller off the navigation stack. You can use this property to retrieve the gesture recognizer and tie it to the behavior of other gesture recognizers in your user interface. When tying your gesture recognizers together, make sure they recognize their gestures simultaneously to ensure that your gesture recognizers are given a chance to handle the event.

So you will have to implement your own UIGestureRecognizer, and tie its behavior to the interactivePopGestureRecognizer of your UIViewController.


Edit :

Here is a solution I built. You can implement your own transition conforming to the UIViewControllerAnimatedTransitioning delegate. This solution works, but has not been thoroughly tested.

You will get an interactive sliding transition to pop your ViewControllers. You can slide to right from anywhere in the view.

Known issue : if you start the pan and stop before half the width of the view, the transition is canceled (expected behavior). During this process, the views reset to their original frames. Their is a visual glitch during this animation.

The classes of the example are the following :

UINavigationController > ViewController > SecondViewController

CustomPopTransition.h :

#import <Foundation/Foundation.h>

@interface CustomPopTransition : NSObject <UIViewControllerAnimatedTransitioning>

@end

CustomPopTransition.m :

#import "CustomPopTransition.h"
#import "SecondViewController.h"
#import "ViewController.h"

@implementation CustomPopTransition

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.3;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {

    SecondViewController *fromViewController = (SecondViewController*)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    ViewController *toViewController = (ViewController*)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toViewController.view];
    [containerView bringSubviewToFront:fromViewController.view];

    // Setup the initial view states
    toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];

    [UIView animateWithDuration:0.3 animations:^{

        fromViewController.view.frame = CGRectMake(toViewController.view.frame.size.width, fromViewController.view.frame.origin.y, fromViewController.view.frame.size.width, fromViewController.view.frame.size.height);

    } completion:^(BOOL finished) {

        // Declare that we've finished
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
    }];

}

@end

SecondViewController.h :

#import <UIKit/UIKit.h>

@interface SecondViewController : UIViewController <UINavigationControllerDelegate>

@end

SecondViewController.m :

#import "SecondViewController.h"
#import "ViewController.h"
#import "CustomPopTransition.h"

@interface SecondViewController ()

@property (nonatomic, strong) UIPercentDrivenInteractiveTransition *interactivePopTransition;

@end

@implementation SecondViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.navigationController.delegate = self;

    UIPanGestureRecognizer *popRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePopRecognizer:)];
    [self.view addGestureRecognizer:popRecognizer];
}

-(void)viewDidDisappear:(BOOL)animated {

    [super viewDidDisappear:animated];

    // Stop being the navigation controller's delegate
    if (self.navigationController.delegate == self) {
        self.navigationController.delegate = nil;
    }
}

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {

    // Check if we're transitioning from this view controller to a DSLSecondViewController
    if (fromVC == self && [toVC isKindOfClass:[ViewController class]]) {
        return [[CustomPopTransition alloc] init];
    }
    else {
        return nil;
    }
}

- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {

    // Check if this is for our custom transition
    if ([animationController isKindOfClass:[CustomPopTransition class]]) {
        return self.interactivePopTransition;
    }
    else {
        return nil;
    }
}

- (void)handlePopRecognizer:(UIPanGestureRecognizer*)recognizer {

    // Calculate how far the user has dragged across the view
    CGFloat progress = [recognizer translationInView:self.view].x / (self.view.bounds.size.width * 1.0);
    progress = MIN(1.0, MAX(0.0, progress));

    if (recognizer.state == UIGestureRecognizerStateBegan) {
        NSLog(@"began");
        // Create a interactive transition and pop the view controller
        self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
        [self.navigationController popViewControllerAnimated:YES];
    }
    else if (recognizer.state == UIGestureRecognizerStateChanged) {
        NSLog(@"changed");
        // Update the interactive transition's progress
        [self.interactivePopTransition updateInteractiveTransition:progress];
    }
    else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {
        NSLog(@"ended/cancelled");
        // Finish or cancel the interactive transition
        if (progress > 0.5) {
            [self.interactivePopTransition finishInteractiveTransition];
        }
        else {
            [self.interactivePopTransition cancelInteractiveTransition];
        }

        self.interactivePopTransition = nil;
    }
}

@end

Solution 2

Create a pan gesture recogniser and move the interactive pop gesture recogniser's targets across.

Add your recogniser to the pushed view controller's viewDidLoad and voila!

let popGestureRecognizer = self.navigationController!.interactivePopGestureRecognizer!
if let targets = popGestureRecognizer.value(forKey: "targets") as? NSMutableArray {
  let gestureRecognizer = UIPanGestureRecognizer()
  gestureRecognizer.setValue(targets, forKey: "targets")
  self.view.addGestureRecognizer(gestureRecognizer)
}

Solution 3

Here's a Swift version of Spynet's answer, with a few modifications. Firstly, I've defined a linear curve for the UIView animation. Secondly, I've added a semi-transparent black background to the view underneath for a better effect. Thirdly, I've subclassed a UINavigationController. This allows the transition to be applied to any "Pop" transition within the UINavigationController. Here's the code:

CustomPopTransition.swift

import UIKit

class CustomPopTransition: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.3
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
            else {
                return
        }

        let containerView = transitionContext.containerView
        containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)

        // Setup the initial view states
        toViewController.view.frame = CGRect(x: -100, y: toViewController.view.frame.origin.y, width: fromViewController.view.frame.size.width, height: fromViewController.view.frame.size.height)

        let dimmingView = UIView(frame: CGRect(x: 0,y: 0, width: toViewController.view.frame.width, height: toViewController.view.frame.height))
        dimmingView.backgroundColor = UIColor.black
        dimmingView.alpha = 0.5

        toViewController.view.addSubview(dimmingView)

        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       delay: 0,
                       options: UIView.AnimationOptions.curveLinear,
                       animations: {
                        dimmingView.alpha = 0
                        toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
                        fromViewController.view.frame = CGRect(x: toViewController.view.frame.size.width, y: fromViewController.view.frame.origin.y, width: fromViewController.view.frame.size.width, height: fromViewController.view.frame.size.height)
        },
                       completion: { finished in
                        dimmingView.removeFromSuperview()
                        transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
        )
    }
}

PoppingNavigationController.swift

import UIKit

class PoppingNavigationController : UINavigationController, UINavigationControllerDelegate {
    var interactivePopTransition: UIPercentDrivenInteractiveTransition!

    override func viewDidLoad() {
        self.delegate = self
    }

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

    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if (operation == .pop) {
            return CustomPopTransition()
        }
        else {
            return nil
        }
    }

    func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        if animationController.isKind(of: CustomPopTransition.self) {
            return interactivePopTransition
        }
        else {
            return nil
        }
    }

    func addPanGesture(viewController: UIViewController) {
        let popRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanRecognizer(recognizer:)))
        viewController.view.addGestureRecognizer(popRecognizer)
    }

    @objc
    func handlePanRecognizer(recognizer: UIPanGestureRecognizer) {
        // Calculate how far the user has dragged across the view
        var progress = recognizer.translation(in: self.view).x / self.view.bounds.size.width
        progress = min(1, max(0, progress))
        if (recognizer.state == .began) {
            // Create a interactive transition and pop the view controller
            self.interactivePopTransition = UIPercentDrivenInteractiveTransition()
            self.popViewController(animated: true)
        }
        else if (recognizer.state == .changed) {
            // Update the interactive transition's progress
            interactivePopTransition.update(progress)
        }
        else if (recognizer.state == .ended || recognizer.state == .cancelled) {
            // Finish or cancel the interactive transition
            if (progress > 0.5) {
                interactivePopTransition.finish()
            }
            else {
                interactivePopTransition.cancel()
            }
            interactivePopTransition = nil
        }
    }
}

Example of the result: enter image description here

Solution 4

There really is no need to roll your own solution for this, sub-classing UINavigationController and referencing the built-in gesture works just fine as explained here.

The same solution in Swift:

public final class MyNavigationController: UINavigationController {

  public override func viewDidLoad() {
    super.viewDidLoad()


    self.view.addGestureRecognizer(self.fullScreenPanGestureRecognizer)
  }

  private lazy var fullScreenPanGestureRecognizer: UIPanGestureRecognizer = {
    let gestureRecognizer = UIPanGestureRecognizer()

    if let cachedInteractionController = self.value(forKey: "_cachedInteractionController") as? NSObject {
      let string = "handleNavigationTransition:"
      let selector = Selector(string)
      if cachedInteractionController.responds(to: selector) {
        gestureRecognizer.addTarget(cachedInteractionController, action: selector)
      }
    }

    return gestureRecognizer
  }()
}

If you do this, also implement the following UINavigationControllerDelegate function to avoid strange behaviour at the root view controller:

public func navigationController(_: UINavigationController,
                                 didShow _: UIViewController, animated _: Bool) {
  self.fullScreenPanGestureRecognizer.isEnabled = self.viewControllers.count > 1
}

Solution 5

Subclassing the UINavigationController you can add a UISwipeGestureRecognizer to trigger the pop action:

.h file:

#import <UIKit/UIKit.h>

@interface CNavigationController : UINavigationController

@end

.m file:

#import "CNavigationController.h"

@interface CNavigationController ()<UIGestureRecognizerDelegate, UINavigationControllerDelegate>

@property (nonatomic, retain) UISwipeGestureRecognizer *swipeGesture;

@end

@implementation CNavigationController

#pragma mark - View cycles

- (void)viewDidLoad {
    [super viewDidLoad];

    __weak CNavigationController *weakSelf = self;
    self.delegate = weakSelf;

    self.swipeGesture = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(gestureFired:)];
    [self.view addGestureRecognizer:self.swipeGesture]; }

#pragma mark - gesture method

-(void)gestureFired:(UISwipeGestureRecognizer *)gesture {
    if (gesture.direction == UISwipeGestureRecognizerDirectionRight)
    {
        [self popViewControllerAnimated:YES];
    } }

#pragma mark - UINavigation Controller delegate

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    self.swipeGesture.enabled = NO;
    [super pushViewController:viewController animated:animated]; }

#pragma mark UINavigationControllerDelegate

- (void)navigationController:(UINavigationController *)navigationController
       didShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animate {
    self.swipeGesture.enabled = YES; }

@end
Share:
13,615

Related videos on Youtube

Chetu
Author by

Chetu

Hi, Working as ios Application developer since oct 2012.Have worked on ObjetiveC,Swift with JSON,API Parsing,InAppPurchase,Many third party payment, PAYPAL, MAP, Music Recognised ,Video and many more.

Updated on June 03, 2022

Comments

  • Chetu
    Chetu almost 2 years

    I want to pop a view when swipe right on screen or it's work like back button of navigation bar.

    I am using:

    self.navigationController.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
    

    This single line of code for pop navigation view and it's a work for me but when i swipe form middle of screen this will not work like Instagram iPhone app.

    Here i give a one screen of Instagram app in that you can see the Example of swipe right pop navigation view:

    enter image description here

  • Chetu
    Chetu about 10 years
    any idea How can i do that?
  • rdurand
    rdurand about 10 years
    @chetu : Nope, sorry.. I'll let you know if I find a way, but your best option is to try by yourself..
  • rdurand
    rdurand about 10 years
    Paste your code in your answer. If your link dies, your answer is useless.
  • rdurand
    rdurand about 10 years
    This solution uses a swipe, so you don't get control over the transition. It won't be interactive, whereas Apple's implementation is.
  • Arun
    Arun about 10 years
    @rdurand i need to support ios6 device that's y i did like that
  • rdurand
    rdurand about 10 years
    That's an acceptable solution in this case. I added a working iOS7+ solution in my answer.
  • Arun
    Arun about 10 years
    @rdurand is it okay now
  • Chetu
    Chetu over 9 years
    github.com/vinqon/MultiLayerNavigation/blob/master/Src/… this will work but limitation is, when we use a slide view controller it's don't work. Ex- pop(back) not working. @dipang
  • heximal
    heximal almost 9 years
    Thanks a lot, chetu. brilliant solution! (MultiLayerNavigation)
  • Alec
    Alec over 8 years
    Works perfect on 9.0. Thank you. I don't see the visual glitch you mentioned either.
  • rdurand
    rdurand over 8 years
    @Alec : it may have been resolved in iOS 8 / 9, since this post is almost 2 years old :) Glad it helped !
  • Tometoyou
    Tometoyou about 8 years
    Do you think you could update this to a swift solution? :)
  • rdurand
    rdurand about 8 years
    @Tometoyou: sorry, I haven't played around with Swift much yet.. But if you give it a go, feel free to edit the post and add the Swift version :)
  • Tometoyou
    Tometoyou almost 7 years
    @Spynet No problem! I just updated the CustomPopTransition.swift code because I had noticed a bug with the other code on here to do with what happens when you drag and then release multiple times before you commit to swiping back...
  • Arun
    Arun almost 7 years
    Good job keep-it up
  • Rikco
    Rikco almost 6 years
    works great! I would recommend: instead of checking if (fromVC == self && [toVC isKindOfClass:[ViewController class]]) I checked if (fromVC == self && operation == UINavigationControllerOperationPop && self.isSwiping == YES) for a more general solution I can use everywhere. The bool isSwiping is for the back-button so that clicking the backbutton will use a default animation. isSwiping will be set within handlePopRecognizer. thx a lot!
  • Serj Rubens
    Serj Rubens over 5 years
    not bad! but animation is working a bit weird. seems like view controller which you're dragging going to final position without animation. for example in instagram or telegram iOS app's it's more smoothie, starting to animate to final position from ~20-30pt and also checking if the direction of swipe changed then smoothly animating view to initial position. + what i also don't like - seems like your code is a bit hacky; looks like you are calling private methods which can be removed by apple. but i voted for it :)
  • jwswart
    jwswart over 5 years
    Did you implement the second part of the answer? I had to do that to avoid any strange behaviour with the animation. I hear you on the private methods but I've submitted an app to the store with this code and it worked just fine. Also check the link in the answer for the original description of this solution (it's not my idea :))
  • jwswart
    jwswart over 5 years
    Ah I see the link is no longer valid for the source of the solution.
  • Serj Rubens
    Serj Rubens over 5 years
    thanks anyway!) this solution is best from the others, also short and universal cuz working with navigation controller not with the UIViewController.
  • Kugutsumen
    Kugutsumen over 4 years
  • Clay Ellis
    Clay Ellis about 4 years
    I laughed when this worked. That's brilliant. Great answer.
  • Kugutsumen
    Kugutsumen almost 3 years
    Thanks, here is a detailed solution stackoverflow.com/a/57487724/1745000