99

I love the swipe pack thats inherited from embedding your views in a UINavigationController. Unfortunately i cannot seem to find a way to hide the NavigationBar but still have the touch pan swipe back gesture. I can write custom gestures but I prefer not to and to rely on the UINavigationController back swipe gesture instead.

if I uncheck it in the storyboard, the back swipe doesn't work

enter image description here

alternatively if I programmatically hide it, the same scenario.

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.navigationController setNavigationBarHidden:YES animated:NO]; // and animated:YES
}

Is there no way to hide the top NavigationBar and still have the swipe?

Manoj Rlogical
  • 239
  • 1
  • 4
mihai
  • 4,184
  • 3
  • 26
  • 27
  • 1
    Is adding a UIGestureRecognizer acceptable? It is a breeze to implement. – SwiftArchitect Jul 12 '14 at 06:35
  • 1
    @LancelotdelaMare, i was trying to avoid that since it wont work as smoothly as the UINavigationController back swipe. Im looking into UIScreenEdgePanGestureRecognizer since some people say it helps but havent gotten it to work yet. Looking for the simplest and most elegant solution here. – mihai Jul 12 '14 at 06:48

19 Answers19

105

A hack that is working is to set the interactivePopGestureRecognizer's delegate of the UINavigationController to nil like this:

[self.navigationController.interactivePopGestureRecognizer setDelegate:nil];

But in some situations it could create strange effects.

nburk
  • 22,409
  • 18
  • 87
  • 132
HorseT
  • 6,592
  • 2
  • 18
  • 16
  • this worked. I added this only to the root view controller in addition to [self.navigationController setNavigationBarHidden:YES animated:NO]; – mihai Jul 23 '14 at 14:52
  • what are some of the adverse affects you have come across? I have not found any yet in this project. – mihai Jul 23 '14 at 16:04
  • 17
    "swiping back repeatedly can cause the gesture to be recognized when there’s only one view controller on the stack, which in turn puts a UI in a (I think unexpected by UIKit engineers) state where it stops recognizing any gestures" – HorseT Jul 24 '14 at 18:22
  • 4
    An alternative that might protect against that unexpected state would be to set it to some low-level object (I used my app delegate) and implement `gestureRecognizerShouldBegin`, returning `true` if the `navigationController`'s `viewController` count is greater than 0. – Kenny Winker Feb 26 '15 at 04:40
  • 4
    Although this works, I HIGHLY recommend against this. Breaking the delegate was causing a rare and hard to identify main thread block. Turns out its not a main thread block but its what @HorseT described. – Josh Bernfeld Aug 07 '15 at 10:03
  • 3
    My app saves the delegate handle then restores it in `viewWillDisappear` and so far have not experienced adverse side-effect. – Don Park Aug 19 '15 at 04:42
  • 1
    !!!! Highly discourage to use this solution , when repeatedly using swipe a strange behaviour occurs, the back is disabled and whole app doens't respond any more – KarimIhab Jun 14 '16 at 13:28
  • Worked like a charm. Saved lot's of time. – adijazz91 Sep 27 '16 at 12:04
  • I have a navigationController in the home screen and I push other navigationController, when I swipe to go back, it doesn't work, because in other navigationController has no more view controller. So, do you have any solution for my case? – Quoc Le Dec 29 '16 at 08:56
105

Problems with Other Methods

Setting interactivePopGestureRecognizer.delegate = nil has unintended side-effects.

Setting navigationController?.navigationBar.hidden = true does work, but does not allow your change in navigation bar to be hidden.

Lastly, it's generally better practice to create a model object that is the UIGestureRecognizerDelegate for your navigation controller. Setting it to a controller in the UINavigationController stack is what causes EXC_BAD_ACCESS errors.

Full Solution

First, add this class to your project:

class InteractivePopRecognizer: NSObject, UIGestureRecognizerDelegate {

    weak var navigationController: UINavigationController

    init(controller: UINavigationController) {
        self.navigationController = controller
    }

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

    // This is necessary because without it, subviews of your top controller can
    // cancel out your gesture recognizer on the edge.
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

Then, set your navigation controller's interactivePopGestureRecognizer.delegate to an instance of your new InteractivePopRecognizer class.

var popRecognizer: InteractivePopRecognizer?

override func viewDidLoad() {
    super.viewDidLoad()
    setInteractiveRecognizer()
}

private func setInteractiveRecognizer() {
    guard let controller = navigationController else { return }
    popRecognizer = InteractivePopRecognizer(controller: controller)
    controller.interactivePopGestureRecognizer?.delegate = popRecognizer
}

Enjoy a hidden navigation bar with no side effects, that works even if your top controller has table, collection, or scroll view subviews.

Hunter Monk
  • 1,967
  • 1
  • 14
  • 25
  • 2
    Great solution! – Matt Butler Feb 09 '17 at 23:52
  • This method worked great for me, thanks! I did a little tweaking to make things a little cleaner (to me and how I'm using it in my project). I made a subclass of UINavigationController to keep the reference to the InteractivePopRecognizer and then just set a custom class for the nav controller in my storyboard. – tylermilner Apr 05 '17 at 01:40
  • Could no longer edit my comment so here's the [link to my modifications](http://stackoverflow.com/a/43220608/4343618) for anyone interested. – tylermilner Apr 05 '17 at 02:16
  • 2
    The best answer, Thanks! – Dory Daniel May 22 '17 at 12:31
  • 3
    @HunterMaximillionMonk thanks for the great solution. It works like a charm – as diu Jul 14 '17 at 20:26
  • 1
    @HunterMaximillionMonk this seems works correctly but the problem with it when i have multiple controllers then after one time pop it stops working. – Premal Khetani Aug 02 '17 at 06:05
  • 2
    Definitely best answer! – daxh Oct 09 '17 at 16:34
  • 1
    This is the one, everybody. – cookednick Nov 07 '17 at 21:46
  • This solution is not completely perfect - I have a custom Navigation Bar and a custom back button on the top left hand corner. This swipe gesture is overriding click events on the back button and makes the tap area of the back button super small. Given the fact that we hide navigation bar mostly to implement our own navigation bar, I think this is a serious problem – Sira Lam Jan 11 '18 at 04:59
  • 1
    @PremalKhetani and for anyone else having the same issue with only being able to go one level down. Run setInteractiveRecognizer() inside viewDidAppear() instead and it will work as intended. – nullforlife Jun 30 '18 at 10:20
  • 3
    +1 I really wish this was the selected answer; saved a ton of headache but I almost missed it because it's further down the page. I'm not too sure stack-overflow has the best set up when the OP gets 100% of power to choose the 'answer'. Are there any meta posts about this? – Albert Renshaw Mar 07 '19 at 23:28
  • Still works in iOS13 but I had to hijack `navigationController.delegate` to check that `interactivePopGestureRecognizer?.delegate` was indeed an `InteractivePopRecognizer` – Workshed Apr 08 '20 at 16:00
  • Not sure if its just me. But I encountered a serious memory leak with this approach. It appears that the popRecognizer still has a strong reference to the navigated page. Even when user navigate back to the for example: CollectionViewController – Legolas Wang May 01 '20 at 04:00
  • 2
    Worked on iOS 13.5, 12.4.6 and 10.3.4. Thanks. – Денис Попов May 26 '20 at 07:10
  • Another possible unwanted side effect: pan gestures are no longer coordinated between any scroll views that you might have on the outgoing VC, so while swiping to pop you will also simultaneously be scrolling the view you started on. You might need to tweak ...shouldRecognizeSimultaneouslyWith... to handle that. – John Scalo Nov 02 '20 at 21:45
  • 1
    **NOTICE:** Make sure you define `navigationController` as a weak variable inside `InteractivePopRecognizer`, otherwise it can cause memory leak and it may become hard to identify and fix later! – Sina KH May 17 '23 at 21:49
62

In my case, to prevent strange effects

Root view controller

override func viewDidLoad() {
    super.viewDidLoad()

    // Enable swipe back when no navigation bar
    navigationController?.interactivePopGestureRecognizer?.delegate = self
}

// UIGestureRecognizerDelegate
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
    if let navVc = navigationController {
      return navVc.viewControllers.count > 1
    }
    return false
}
saranpol
  • 2,177
  • 1
  • 23
  • 22
  • 2
    Sometimes I'm getting EXC_BAD_ACCESS when using this – Andrey Gordeev Aug 05 '15 at 11:07
  • For me, it doesn't make the gesture work and frequently crashes with the `EXEC_BAD_ACCESS` – Benjohn Sep 30 '15 at 10:29
  • 2
    Remember to add `UIGestureRecognizerDelegate` to the root view controller... In my case the delegate was set to nil in a later view controller than the root view controller, so when returned to the root view controller, `gestureRecognizerShouldBegin` wasn't called. So I placed the `.delegate = self` in `viewDidAppear()`. That solved the strange effects in my case.. Cheers! – Wiingaard Nov 20 '15 at 14:51
  • @AndreyGordeev Could you please give some details about when `EXEC_BAD_ACCESS` happens? – Jaybo Jan 18 '16 at 16:19
  • Here's more info about `EXC_BAD_ACCESS` error: http://stackoverflow.com/questions/28746123/viewcontroller-gesturerecognizershouldrecognizesimultaneouslywithgesturerecogn – Andrey Gordeev Mar 11 '16 at 12:59
  • I ended with the following solution: set a `delegate` at `viewWillAppear` and nil it out at `viewWillDisappear`. Seems working – Andrey Gordeev Mar 11 '16 at 13:01
  • I had the freeze issue by setting only the delegate. After seeing this answer I implemented `gestureRecognizerShouldBegin` and let it return always `true`, while printing count, just to confirm that this is the issue - and it didn't happen again! So apparently simply implementing the recogniser fixes it, though I'll add the count check just in case. It can be also that I haven't tested long enough. – User Jun 11 '16 at 11:47
  • Works great! Although I needed to add `func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true //return NSStringFromClass(otherGestureRecognizer.dynamicType).containsString("UIScrollViewPanGestureRecognizer"); }` to stop scrolling the webview while doing the swipe back – Kamen Dobrev Jun 22 '16 at 16:55
  • Just a style comment: In the recognizer method you could just make that `return navigationController!.viewControllers.count > 1` – Pat Niemeyer Nov 11 '16 at 16:59
  • Works great, for now. Thanks! – Teodor Ciuraru Jan 21 '17 at 10:00
  • you should guard in the gesture func: func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard let navigationController = navigationController else { return false } if(navigationController.viewControllers.count > 1) { return true } return false } – crizzwald Jun 28 '17 at 16:00
30

Updated for iOS 13.4

iOS 13.4 broke the previous solution, so things are gonna get ugly. It looks like in iOS 13.4 this behavior is now controlled by a private method _gestureRecognizer:shouldReceiveEvent: (not to be confused with the new public shouldReceive method added in iOS 13.4).


I found that other posted solutions overriding the delegate, or setting it to nil caused some unexpected behavior.

In my case, when I was on the top of the navigation stack and tried to use the gesture to pop one more, it would fail (as expected), but subsequent attempts to push onto the stack would start to cause weird graphical glitches in the navigation bar. This makes sense, because the delegate is being used to handle more than just whether or not to block the gesture from being recognized when the navigation bar is hidden, and all that other behavior was being thrown out.

From my testing, it appears that gestureRecognizer(_:, shouldReceiveTouch:) is the method that the original delegate is implementing to block the gesture from being recognized when the navigation bar is hidden, not gestureRecognizerShouldBegin(_:). Other solutions that implement gestureRecognizerShouldBegin(_:) in their delegate work because the lack of an implementation of gestureRecognizer(_:, shouldReceiveTouch:) will cause the default behavior of receiving all touches.

@Nathan Perry's solution gets close, but without an implementation of respondsToSelector(_:), the UIKit code that sends messages to the delegate will believe there is no implementation for any of the other delegate methods, and forwardingTargetForSelector(_:) will never get called.

So, we take control of `gestureRecognizer(_:, shouldReceiveTouch:) in the one specific scenario we want to modify the behavior, and otherwise forward everything else to the delegate.

class AlwaysPoppableNavigationController : UINavigationController {

    private var alwaysPoppableDelegate: AlwaysPoppableDelegate!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.alwaysPoppableDelegate = AlwaysPoppableDelegate(navigationController: self, originalDelegate: self.interactivePopGestureRecognizer!.delegate!)
        self.interactivePopGestureRecognizer!.delegate = self.alwaysPoppableDelegate
    }
}

private class AlwaysPoppableDelegate : NSObject, UIGestureRecognizerDelegate {

    weak var navigationController: AlwaysPoppableNavigationController?
    weak var originalDelegate: UIGestureRecognizerDelegate?

    init(navigationController: AlwaysPoppableNavigationController, originalDelegate: UIGestureRecognizerDelegate) {
        self.navigationController = navigationController
        self.originalDelegate = originalDelegate
    }

    // For handling iOS before 13.4
    @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if let navigationController = navigationController, navigationController.isNavigationBarHidden && navigationController.viewControllers.count > 1 {
            return true
        }
        else if let originalDelegate = originalDelegate {
            return originalDelegate.gestureRecognizer!(gestureRecognizer, shouldReceive: touch)
        }
        else {
            return false
        }
    }

    // For handling iOS 13.4+
    @objc func _gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceiveEvent event: UIEvent) -> Bool {
        if let navigationController = navigationController, navigationController.isNavigationBarHidden && navigationController.viewControllers.count > 1 {
            return true
        }
        else if let originalDelegate = originalDelegate {
            let selector = #selector(_gestureRecognizer(_:shouldReceiveEvent:))
            if originalDelegate.responds(to: selector) {
                let result = originalDelegate.perform(selector, with: gestureRecognizer, with: event)
                return result != nil
            }
        }

        return false
    }

    override func responds(to aSelector: Selector) -> Bool {
        if #available(iOS 13.4, *) {
            // iOS 13.4+ does not need to override responds(to:) behavior, it only uses forwardingTarget
            return originalDelegate?.responds(to: aSelector) ?? false
        }
        else {
            if aSelector == #selector(gestureRecognizer(_:shouldReceive:)) {
                return true
            }
            else {
                return originalDelegate?.responds(to: aSelector) ?? false
            }
        }
    }

    override func forwardingTarget(for aSelector: Selector) -> Any? {
        if #available(iOS 13.4, *), aSelector == #selector(_gestureRecognizer(_:shouldReceiveEvent:)) {
            return nil
        }
        else {
            return self.originalDelegate
        }
    }
}
Chris Vasselli
  • 13,064
  • 4
  • 46
  • 49
  • 1
    Looks like your solution is the best for this moment. Thanks! – Timur Bernikovich Jan 27 '17 at 13:37
  • "but subsequent attempts to push onto the stack would start to cause weird graphical glitches in the navigation bar" - I'm confused here. I thought we had no navigation bar? That's the question? In my situation, I have a navigation controller embedded as a child view controller with no navbar; the containing VC has the navigation controls. So I've let the containing VC be the recognizer's delegate and just did the `gestureRecognizerShouldBegin:` thing, and it "seems to work". Wondering would I should look out for. – skagedal Feb 02 '17 at 17:04
  • 2
    This had a memory leak since the `navigationController` was a strong reference in the AlwaysPoppableDelegate. I've edited the code to make this a `weak` reference. – Graham Perks Jun 01 '17 at 20:21
  • 4
    This nice solution doesn't work anymore in iOS 13.4 – Ely Mar 29 '20 at 11:53
  • @ChrisVasselli Really awesome, thank you! Hopefully this will pass the private methods check of App Store review. – Ely Apr 11 '20 at 09:12
  • @GrahamPerks I think it needs to be strong to keep the retain count above zero... – steven May 13 '20 at 10:06
16

You can subclass UINavigationController as following:

@interface CustomNavigationController : UINavigationController<UIGestureRecognizerDelegate>

@end

Implementation:

@implementation CustomNavigationController

- (void)setNavigationBarHidden:(BOOL)hidden animated:(BOOL)animated {
    [super setNavigationBarHidden:hidden animated:animated];
    self.interactivePopGestureRecognizer.delegate = self;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    if (self.viewControllers.count > 1) {
        return YES;
    }
    return NO;
}

@end
Yogesh Maheshwari
  • 1,324
  • 2
  • 16
  • 37
  • 2
    Using this approach is breaking pop gesture in `UIPageViewController` overscroll. – atulkhatri Mar 03 '16 at 14:30
  • I found that viewController.count > 1 check is necessary. If the user attempts to swipe back with only the root VC, the UI would hang on next VC push. – VaporwareWolf Jan 05 '17 at 04:53
15

Simple, no side-effect Answer

While most answers here are good, they seemingly have unintended side-effects (app breaking) or are verbose.

The most simple yet functional solution I could come up with was the following:

In the ViewController that you are hiding the navigationBar,

class MyNoNavBarViewController: UIViewController {
    
    // needed for reference when leaving this view controller
    var initialInteractivePopGestureRecognizerDelegate: UIGestureRecognizerDelegate?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // we will need a reference to the initial delegate so that when we push or pop.. 
        // ..this view controller we can appropriately assign back the original delegate
        initialInteractivePopGestureRecognizerDelegate = self.navigationController?.interactivePopGestureRecognizer?.delegate
    }

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

        // we must set the delegate to nil whether we are popping or pushing to..
        // ..this view controller, thus we set it in viewWillAppear()
        self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
    }

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

        // and every time we leave this view controller we must set the delegate back..
        // ..to what it was originally
        self.navigationController?.interactivePopGestureRecognizer?.delegate = initialInteractivePopGestureRecognizerDelegate
    }
}

Other answers have suggested merely setting the delegate to nil. Swiping backwards to the initial view controller on the navigation stack results in all gestures to be disabled. Some sort of oversight, perhaps, of the UIKit/UIGesture devs.

As well, some answers here that I have implemented resulted in non-standard apple navigation behaviour (specifically, allowing for the ability to scroll up or down while also swiping backwards). These answers also seem a bit verbose and in some cases incomplete.

Community
  • 1
  • 1
CodyB
  • 151
  • 1
  • 4
  • `viewDidLoad()` is not a good place to capture `initialInteractivePopGestureRecognizerDelegate` since `navigationController` could be nil there (not pushed onto stack yet). `viewWillAppear` of place where you are hiding navigation bar would be more appropriate – nCod3d Apr 28 '20 at 10:29
  • Thank you, Best & simple solution in all above answers – Mehul Jul 06 '21 at 12:20
12

Building off of Hunter Maximillion Monk's answer, I made a subclass for UINavigationController and then set the custom class for my UINavigationController in my storyboard. Final code for the two classes looks like this:

InteractivePopRecognizer:

class InteractivePopRecognizer: NSObject {

    // MARK: - Properties

    fileprivate weak var navigationController: UINavigationController?

    // MARK: - Init

    init(controller: UINavigationController) {
        self.navigationController = controller

        super.init()

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

extension InteractivePopRecognizer: UIGestureRecognizerDelegate {
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return (navigationController?.viewControllers.count ?? 0) > 1
    }

    // This is necessary because without it, subviews of your top controller can cancel out your gesture recognizer on the edge.
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

HiddenNavBarNavigationController:

class HiddenNavBarNavigationController: UINavigationController {

    // MARK: - Properties

    private var popRecognizer: InteractivePopRecognizer?

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        setupPopRecognizer()
    }

    // MARK: - Setup

    private func setupPopRecognizer() {
        popRecognizer = InteractivePopRecognizer(controller: self)
    }
}

Storyboard:

Storyboard nav controller custom class

Community
  • 1
  • 1
tylermilner
  • 434
  • 4
  • 7
8

Looks like solution provided by @ChrisVasseli is the best. I'd like to provide same solution in Objective-C because question is about Objective-C (see tags)

@interface InteractivePopGestureDelegate : NSObject <UIGestureRecognizerDelegate>

@property (nonatomic, weak) UINavigationController *navigationController;
@property (nonatomic, weak) id<UIGestureRecognizerDelegate> originalDelegate;

@end

@implementation InteractivePopGestureDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    if (self.navigationController.navigationBarHidden && self.navigationController.viewControllers.count > 1) {
        return YES;
    } else {
        return [self.originalDelegate gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch];
    }
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if (aSelector == @selector(gestureRecognizer:shouldReceiveTouch:)) {
        return YES;
    } else {
        return [self.originalDelegate respondsToSelector:aSelector];
    }
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.originalDelegate;
}

@end

@interface NavigationController ()

@property (nonatomic) InteractivePopGestureDelegate *interactivePopGestureDelegate;

@end

@implementation NavigationController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.interactivePopGestureDelegate = [InteractivePopGestureDelegate new];
    self.interactivePopGestureDelegate.navigationController = self;
    self.interactivePopGestureDelegate.originalDelegate = self.interactivePopGestureRecognizer.delegate;
    self.interactivePopGestureRecognizer.delegate = self.interactivePopGestureDelegate;
}

@end
Timur Bernikovich
  • 5,660
  • 4
  • 45
  • 58
7

The answer of Hunter Monk is really awesome, but unfortunately in iOS 13.3.1, it does not work.

I will explain another way to hide UINavigationBar and not lose swipe to back gesture. I have tested on iOS 13.3.1 and 12.4.3 and it works.

You need to create a custom class of UINavigationController and set that class for UINavigationController in Storyboard

Set custom class to <code>UINavigationController</code>

Do NOT hide the NavigationBar on the Storyboard

<code>UINavigationController</code> Attributes inspector:

Example on Storyboard:

Storyboard:

And finally, put this: navigationBar.isHidden = true in viewDidLoad of CustomNavigationController class.

Make sure, do NOT use this method setNavigationBarHidden(true, animated: true) for hiding the NavigationBar.

import UIKit

class CustomNavigationController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationBar.isHidden = true
    }
}
Jusuf Saiti
  • 127
  • 2
  • 7
  • 2
    I've tested this on real device iPhone 6S Plus with `iOS 13.4.1` and swipe back works. – Emre Aydin Jul 05 '20 at 15:40
  • 2
    Nice solution, tested on iOS 14.5 (beta 2) and still working. Just keep in mind that preferredStatusBarStyle will not be called in the view controllers anymore. It has to be handled by the custom navigation controller. – diegotrevisan Feb 20 '21 at 09:59
6

My solution is to directly extend the UINavigationController class :

import UIKit

extension UINavigationController: UIGestureRecognizerDelegate {

    override open func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        self.interactivePopGestureRecognizer?.delegate = self
    }

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

}

This way, all navigation controllers will be dismissable by sliding.

fredericdnd
  • 968
  • 1
  • 14
  • 25
  • Oddly enough this is causing all the `viewDidAppear` calls on the VCs belonging to any navigation controller to be ignored. – cumanzor Mar 08 '20 at 20:42
4

You can do it with a Proxy Delegate. When you are building the navigation controller, grab the existing delegate. And pass it into the proxy. Then pass all delegate methods to the existing delegate except gestureRecognizer:shouldReceiveTouch: using forwardingTargetForSelector:

Setup:

let vc = UIViewController(nibName: nil, bundle: nil)
let navVC = UINavigationController(rootViewController: vc)
let bridgingDelegate = ProxyDelegate()
bridgingDelegate.existingDelegate = navVC.interactivePopGestureRecognizer?.delegate
navVC.interactivePopGestureRecognizer?.delegate = bridgingDelegate

Proxy Delegate:

class ProxyDelegate: NSObject, UIGestureRecognizerDelegate {
    var existingDelegate: UIGestureRecognizerDelegate? = nil

    override func forwardingTargetForSelector(aSelector: Selector) -> AnyObject? {
        return existingDelegate
    }

    func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
        return true
    }  
}
Nathan Perry
  • 291
  • 2
  • 11
3

Here is my solution: I am changing alpha on the navigation bar, but the navigation bar is not hidden. All my view controllers are a subclass of my BaseViewController, and there I have:

    override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    navigationController?.navigationBar.alpha = 0.0
}

You could also subclass UINavigationController and put that method there.

2

TLDR- Solution without any side effects:

Instead of creating UINavigationController from storyboard, create a custom class inheriting UINavigationController and present it via code.

class RootNavigationController: UINavigationController {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationBar.isHidden = true
    }
}

let rootNavVC = RootNavigationController(rootViewController: vc)
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
    appDelegate.window?.rootViewController = rootNavVC
}

Other solutions tried:

  1. interactivePopGestureRecognizer.delegate = nil resulted random behaviour.

  2. Setting interactivePopGestureRecognizer.delegate = self and then doing this in viewDidAppear or at some other place.

    if navigationController?.viewControllers.count ?? 0 > 1 { navigationController?.interactivePopGestureRecognizer?.isEnabled = true } else { navigationController?.interactivePopGestureRecognizer?.isEnabled = false }

This worked fine as long as there were more than 1 viewControllers in the stack. App freezes if the count is <= 1.

1

Xamarin Answer:

Implement the IUIGestureRecognizerDelegate Interface in your ViewController's Class definition:

public partial class myViewController : UIViewController, IUIGestureRecognizerDelegate

In your ViewController add the following method:

[Export("gestureRecognizerShouldBegin:")]
public bool ShouldBegin(UIGestureRecognizer recognizer) {
  if (recognizer is UIScreenEdgePanGestureRecognizer && 
      NavigationController.ViewControllers.Length == 1) {
    return false;
  }
  return true;
}

In your ViewController's ViewDidLoad() add the following line :

NavigationController.InteractivePopGestureRecognizer.Delegate = this;
Community
  • 1
  • 1
Ahmad
  • 603
  • 1
  • 8
  • 17
1

I've tried this and it's working perfectly : How to hide Navigation Bar without losing slide-back ability

The idea is to implement "UIGestureRecognizerDelegate" in your .h and add this to your .m file.

- (void)viewWillAppear:(BOOL)animated {
// hide nav bar
[[self navigationController] setNavigationBarHidden:YES animated:YES];

// enable slide-back
if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    self.navigationController.interactivePopGestureRecognizer.enabled = YES;
    self.navigationController.interactivePopGestureRecognizer.delegate = self;
  }
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
   return YES;  
}
Community
  • 1
  • 1
KarimIhab
  • 173
  • 1
  • 8
0

Some people have had success by calling the setNavigationBarHidden method with animated YES instead.

Community
  • 1
  • 1
Mundi
  • 79,884
  • 17
  • 117
  • 140
0

In my view controller without navigationbar I use

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

  CATransaction.begin()
  UIView.animate(withDuration: 0.25, animations: { [weak self] in
    self?.navigationController?.navigationBar.alpha = 0.01
  })
  CATransaction.commit()
}

open override func viewWillDisappear(_ animated: Bool) {
  super.viewWillDisappear(animated)
  CATransaction.begin()
  UIView.animate(withDuration: 0.25, animations: { [weak self] in
    self?.navigationController?.navigationBar.alpha = 1.0
  })
  CATransaction.commit()
}

During the interactive dismissal the back button will shine through though, which is why I hid it.

fruitcoder
  • 1,073
  • 8
  • 24
-2

There is a really simple solution that I tried and works perfectly, this is in Xamarin.iOS but can be applied to native too:

    public override void ViewWillAppear(bool animated)
    {
        base.ViewWillAppear(animated);
        this.NavigationController.SetNavigationBarHidden(true, true);
    }

    public override void ViewDidAppear(bool animated)
    {
        base.ViewDidAppear(animated);
        this.NavigationController.SetNavigationBarHidden(false, false);
        this.NavigationController.NavigationBar.Hidden = true;
    }

    public override void ViewWillDisappear(bool animated)
    {
        base.ViewWillDisappear(animated);
        this.NavigationController.SetNavigationBarHidden(true, false);
    }
João Palma
  • 66
  • 11
-6

Here is how to disable de gesture recognizer when user slides out of the ViewController. You can paste it on your viewWillAppear() or on your ViewDidLoad() methods.

if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
Eddwin Paz
  • 2,842
  • 4
  • 28
  • 48