98

I got the opposite issue from here. By default in iOS7, back swipe gesture of UINavigationController's stack could pop the presented ViewController. Now I just uniformed all the self.navigationItem.leftBarButtonItem style for all the ViewControllers.

Here is the code:

self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithImage:LOADIMAGE(@"back_button") style:UIBarButtonItemStylePlain target:self action:@selector(popCurrentViewController)];

after that, the navigationController.interactivePopGestureRecognizer is disabled. How could I make the pop gesture enabled without removing the custom leftBarButtonItem?

Thanks!

Community
  • 1
  • 1
Itachi
  • 5,777
  • 2
  • 37
  • 69
  • The same problem already have solution [here](http://stackoverflow.com/questions/20992039/uinavigationcontroller-interactivepopgesturerecognizer-working-abnormal-in-ios7) – ian Jan 22 '16 at 09:17
  • @ian thanks! It means all the screen swipe gesture is for back swipe, I don't think it's a good idea. – Itachi Jan 22 '16 at 09:51

17 Answers17

91

It works for me when I set the delegate

self.navigationController.interactivePopGestureRecognizer.delegate = self;

and then implement

Swift

extension MyViewController:UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

Objective-C

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}
Husam
  • 8,149
  • 3
  • 38
  • 45
hfossli
  • 22,616
  • 10
  • 116
  • 130
  • 1
    Can you point to any apple documentation that mentions this in relation to the 'swipe to go back' feature? – qix Dec 14 '16 at 06:23
  • https://developer.apple.com/reference/uikit/uinavigationcontroller/1621847-interactivepopgesturerecognizer – BananaNeil Mar 22 '17 at 04:42
  • 2
    Unfortunately this will freeze up if you swipe during a push animation or while you're on the root view controller. I posted a fix version here: http://stackoverflow.com/a/43433530/308315 – iwasrobbed Apr 16 '17 at 03:58
  • 2
    this works! but why? Can any one provide more explanations? Why does requiring this gesture recognizer to fail by another gesture recognizer somehow magically makes it to actually recognize the gesture?? – igrek Jun 28 '18 at 09:18
  • 1
    Still saving lives! – Codetard Sep 18 '20 at 16:31
90

You need to handle two scenarios:

  1. When you're pushing a new view onto the stack
  2. When you're showing the root view controller

If you just need a base class you can use, here's a Swift 3 version:

import UIKit

final class SwipeNavigationController: UINavigationController {
    
    // MARK: - Lifecycle
    
    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)

         delegate = self
    }
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        
        delegate = self
    }

    required init?(coder aDecoder: NSCoder) { 
        super.init(coder: aDecoder) 

        delegate = self 
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // This needs to be in here, not in init
        interactivePopGestureRecognizer?.delegate = self
    }
    
    deinit {
        delegate = nil
        interactivePopGestureRecognizer?.delegate = nil
    }
    
    // MARK: - Overrides
    
    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        duringPushAnimation = true
        
        super.pushViewController(viewController, animated: animated)
    }
    
    // MARK: - Private Properties
    
    fileprivate var duringPushAnimation = false

}

// MARK: - UINavigationControllerDelegate

extension SwipeNavigationController: UINavigationControllerDelegate {
    
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
        
        swipeNavigationController.duringPushAnimation = false
    }
    
}

// MARK: - UIGestureRecognizerDelegate

extension SwipeNavigationController: UIGestureRecognizerDelegate {
    
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        guard gestureRecognizer == interactivePopGestureRecognizer else {
            return true // default value
        }
        
        // Disable pop gesture in two situations:
        // 1) when the pop animation is in progress
        // 2) when user swipes quickly a couple of times and animations don't have time to be performed
        return viewControllers.count > 1 && duringPushAnimation == false
    }
}

If you end up needing to act as a UINavigationControllerDelegate in another class, you can write a delegate forwarder similar to this answer.

Adapted from source in Objective-C: https://github.com/fastred/AHKNavigationController

user1447414
  • 1,306
  • 2
  • 12
  • 25
iwasrobbed
  • 46,496
  • 21
  • 150
  • 195
  • This is a great and clean solution – Andrea.Ferrando May 22 '17 at 16:36
  • 8
    It does work great, thanks - but you could/should also implement the ```required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) delegate = self }``` for use with storyboards :) – smat88dd Jun 09 '17 at 21:34
  • I added storyboard support: https://stackoverflow.com/a/49750785/129202 Seems to work but feel free to edit it to fix any problems. – Jonny Apr 10 '18 at 10:02
  • 1
    What if I don't need swipe to back for certain screens. What I need to do in that screens? I have returned false for this delegate method. `func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {` and set true false in viewwillAppear and disappear methods `self.navigationController?.interactivePopGestureRecognizer?.isEnabled` none worked. any solutions? – Sarath Apr 02 '19 at 13:21
  • 2
    How do I call this class? – Marlhex Oct 23 '19 at 23:43
  • @iwasrobbed, is there a way I can do something like this on a view that was presented? Having to implement the left to right drag to dismiss the root controller. I will really appreciate your response to this, please. – Israel Meshileya Nov 14 '19 at 07:55
  • It's crashing for me on iPad '-[SwipeNavigationController pushAnimationStyle:]: unrecognized selector sent to instance 0x10f836600' – Sanket_B Jul 22 '22 at 17:19
  • works flawlessly to this day (iOS 16 and xcode 14.2) – Radu Ursache Nov 01 '22 at 17:32
  • What's the point of nilify the `delegate`s in the `deinit`? – Giorgio Dec 13 '22 at 15:53
88

First set delegate in viewDidLoad:

self.navigationController.interactivePopGestureRecognizer.delegate = self;

And then disable gesture when pushing:

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

And enable in viewDidDisappear:

self.navigationController.interactivePopGestureRecognizer.enabled = YES;

Also, add UINavigationControllerDelegate to your view controller.

Rakesha Shastri
  • 11,053
  • 3
  • 37
  • 50
Lumialxk
  • 6,239
  • 6
  • 24
  • 47
  • 5
    I forgot to say you should add UINavigationControllerDelegate to your view controller. – Lumialxk Jan 22 '16 at 09:20
  • Thanks! I just found the solution in SO just now, too! (I didn't search enough result before asking, my bad!) – Itachi Jan 22 '16 at 09:48
  • 3
    Why do you set the delegate of the recognizer? You don't mention any implementation of the protocols methods? – hfossli Feb 02 '16 at 19:12
  • 1
    this does not work. @hfossli's solution works perfectly. Although I tried in Swift3. – Shubham Naik Feb 22 '17 at 05:07
  • How can we make this generic and add in an existing project? We sure don't want to do this with every viewcontroller... – Amber K Sep 24 '18 at 13:27
  • @AmberK Try use method – Lumialxk Sep 24 '18 at 13:44
  • @Lumialxk did you mean reuse method? I'm thinking how will it be done without we can't implement this without overriding life cycle method or maybe having a parent view controller.. Yea thats it. thanks... – Amber K Sep 24 '18 at 13:49
  • @AmberK no, I meant method swizzling. With this Objective-C feature, you can add operations before/after method implement dynamicly. – Lumialxk Sep 24 '18 at 13:52
  • I've heard about it..but I am not sure swift has it or not..Thanks will try that.. :) – Amber K Sep 24 '18 at 14:29
56

it works for me Swift 3:

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }

and in ViewDidLoad:

    self.navigationController?.interactivePopGestureRecognizer?.delegate = self
    self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
Yahya Tabba
  • 561
  • 4
  • 6
  • 9
    Also If you have a UIViewController, you should inherit from UIGestureRecognizerDelegate too. – haris Feb 04 '17 at 14:05
  • 8
    This worked for me, but I didn't need to use the `.isEnabled` line. – Clifton Labrum Mar 15 '17 at 05:51
  • 4
    Unfortunately this will freeze up if you swipe during a push animation or while you're on the root view controller. I posted a fix version here: http://stackoverflow.com/a/43433530/308315 – iwasrobbed Apr 16 '17 at 03:57
15

This is the best way to enable/ disable swipe to pop view controller in iOS 10, Swift 3 :

For First Screen [ Where you want to Disable Swipe gesture ] :

class SignUpViewController : UIViewController,UIGestureRecognizerDelegate {

//MARK: - View initializers
override func viewDidLoad() {
    super.viewDidLoad()
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    swipeToPop()
}

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

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
}

func swipeToPop() {

    self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true;
    self.navigationController?.interactivePopGestureRecognizer?.delegate = self;
}

func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {

    if gestureRecognizer == self.navigationController?.interactivePopGestureRecognizer {
        return false
    }
    return true
} }

For middle screen [ Where you want to Enable Swipe gesture ] :

class FriendListViewController : UIViewController {

//MARK: - View initializers
override func viewDidLoad() {

    super.viewDidLoad()
    swipeToPop()
}

func swipeToPop() {

    self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true;
    self.navigationController?.interactivePopGestureRecognizer?.delegate = nil;
} }
Mr. Bean
  • 4,221
  • 1
  • 19
  • 34
  • 3
    Unfortunately this will freeze up if you swipe during a push animation or while you're on the root view controller. I posted a fix version here: http://stackoverflow.com/a/43433530/308315 – iwasrobbed Apr 16 '17 at 03:56
  • @iwasrobbed i checked the above code on root view controller and it worked fine by me i don't know why it failed on your end. Thanks for the updated version – Mr. Bean Apr 18 '17 at 05:14
  • 1
    This is perfect answer , @iwasrobbed You code is not working – Nitin Bhatt Jul 18 '17 at 11:17
  • @NitinBhatt, cn i pls have my +1 if the answer worked for you. Thanks – Mr. Bean Jul 18 '17 at 12:06
14

I did not need to add gestureRecognizer functions for it. It was enough for me to add following code blocks at viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
    self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}
Burcu Kutluay
  • 472
  • 6
  • 14
11

In Swift you can do the following code

import UIKit
extension UINavigationController: UIGestureRecognizerDelegate {

    open override func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }
}

Above code helps in swift left to go back to previous controller like Facebook, Twitter.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
CrazyPro007
  • 1,006
  • 9
  • 15
8

Swift 3:

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

    self.navigationController?.interactivePopGestureRecognizer?.delegate = self
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return (otherGestureRecognizer is UIScreenEdgePanGestureRecognizer)
}
Josh O'Connor
  • 4,694
  • 7
  • 54
  • 98
  • 3
    Unfortunately this will freeze up if you swipe during a push animation or while you're on the root view controller. I posted a fix version here: http://stackoverflow.com/a/43433530/308315 – iwasrobbed Apr 16 '17 at 03:56
8

If you want this behaviour everywhere in your app and don't want to add anything to individual viewDidAppear etc. then you should create a subclass

class QFNavigationController:UINavigationController, UIGestureRecognizerDelegate, UINavigationControllerDelegate{
    override func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
        delegate = self
    }

    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        super.pushViewController(viewController, animated: animated)
        interactivePopGestureRecognizer?.isEnabled = false
    }

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        interactivePopGestureRecognizer?.isEnabled = true
    }

    // IMPORTANT: without this if you attempt swipe on
    // first view controller you may be unable to push the next one
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }

}

Now, whenever you use QFNavigationController you get the desired experience.

Andriy Gordiychuk
  • 6,163
  • 1
  • 24
  • 59
5

I've created the following Swift 5+ UIViewController extension to make it easier to add/remove the interactive pop gesture on each screen that you need it on.

Note:

  • Add enableInteractivePopGesture() on each screen that has your custom back button

  • Add disableInteractivePopGesture() on viewDidAppear for root screen of your navigation controller to prevent the swipe back issue some of the answers here mention

  • Also add disableInteractivePopGesture() on pushed screens that you don't want to have the back button and swipe back gesture

    extension UIViewController: UIGestureRecognizerDelegate {
    
      func disableInteractivePopGesture() {
        navigationItem.hidesBackButton = true
        navigationController?.interactivePopGestureRecognizer?.delegate = self
        navigationController?.interactivePopGestureRecognizer?.isEnabled = false
      }
    
      func enableInteractivePopGesture() {
        navigationController?.interactivePopGestureRecognizer?.delegate = self
        navigationController?.interactivePopGestureRecognizer?.isEnabled = true
      }
    }
    
budiDino
  • 13,044
  • 8
  • 95
  • 91
4

Setting a custom back button disable the swipe back feature.

The best thing to do to keep it is to subclass UINavigationViewController and set itself as the interactivePopGestureRecognizer delegate; then you can return YES from gestureRecognizerShouldBegin to allow the swipe back.

For example, this is done in AHKNavigationController

And a Swift version here: https://stackoverflow.com/a/43433530/308315

Community
  • 1
  • 1
Axel Guilmin
  • 11,454
  • 9
  • 54
  • 64
4

Swift 5, add only these two in viewDidLoad method:

override func viewDidLoad() {
    super.viewDidLoad()

    navigationController?.interactivePopGestureRecognizer?.delegate = self
    navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}
Egzon P.
  • 4,498
  • 3
  • 32
  • 31
3

This answer, but with storyboard support.

class SwipeNavigationController: UINavigationController {

    // MARK: - Lifecycle

    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

        self.setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        self.setup()
    }

    private func setup() {
        delegate = self
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // This needs to be in here, not in init
        interactivePopGestureRecognizer?.delegate = self
    }

    deinit {
        delegate = nil
        interactivePopGestureRecognizer?.delegate = nil
    }

    // MARK: - Overrides

    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        duringPushAnimation = true

        super.pushViewController(viewController, animated: animated)
    }

    // MARK: - Private Properties

    fileprivate var duringPushAnimation = false
}
Jonny
  • 15,955
  • 18
  • 111
  • 232
2

I was having issue to enable and disable swipe interaction to pop viewcontrollers.

I have a base navigation controller and my app flow is like push Splash VC, push Main VC, and then push Some VC like that.

I want swipe to go back from Some VC to Main VC. Also disable swipe to prevent going back to splash from main VC.

After some tryings below works for me.

  1. Write an extension in Main VC to disable swipe
extension MainViewController : UIGestureRecognizerDelegate{
    
    func disableSwipeToPop() {
        self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
        self.navigationController?.interactivePopGestureRecognizer?.delegate = self
    }
    
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        
        if gestureRecognizer == self.navigationController?.interactivePopGestureRecognizer {
            return false
        }
        return true
    }
}
  1. Call disableSwipeToPop() method on viewDidAppear of Main VC
override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.disableSwipeToPop()
}
  1. Write an extension in Some VC to enable swipe to pop Some VC
extension SomeViewController{
    
    func enableSwipeToPop() {
        self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
        self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
    }
    
}
  1. Call enableSwipeToPop() method on viewDidLoad of Some VC
override func viewDidLoad() {
        super.viewDidLoad()
        self.enableSwipeToPop()
}

That's it. Also if you try to disable swipe on viewWillAppear, you may loose the ability to swipe again when user stops swiping to cancel the action.

0

We're all working around some old bugs that haven't been fixed likely because it's "by design." I ran into the freezing problem @iwasrobbed described elsewhere when trying to nil the interactivePopGestureRecognizer's delegate which seemed like it should've worked. If you want swipe behavior reconsider using backBarButtonItem which you can customize.

I also ran into interactivePopGestureRecognizer not working when the UINavigationBar is hidden. If hiding the navigation bar is a concern for you, reconsider your design before implementing a workaround for a bug.

slythfox
  • 545
  • 6
  • 12
0

Most answers are pertaining to doing it on code. But I'll give you one that works on Storyboard. Yes! You read it right.

  • Click on main UINavigationController and navigate to it's Identity Inspector tab.

  • Under User Defined Runtime Attributes, set a single runtime property called interactivePopGestureRecognizer.enabled to true. Or graphically, you'd have to enable the checkbox as shown in the image below.

That's it. You're good to go. Your back gesture will work as if it was there all along.

Image displaying the property that out to be set

mzaink
  • 261
  • 4
  • 10
-1

For those who are still having trouble with this, try separating the two lines as below.

override func viewDidLoad() {
    self.navigationController!.interactivePopGestureRecognizer!.delegate = self
    ...

override func viewWillAppear(_ animated: Bool) {
    self.navigationController!.interactivePopGestureRecognizer!.isEnabled = true
    ...

Obviously, in my app,

interactivePopGestureRecognizer!.isEnabled

got reset to false before the view was shown for some reason.

HDJEMAI
  • 9,436
  • 46
  • 67
  • 93
Izumi.H
  • 147
  • 1
  • 4