在 UINavigationController 中隐藏导航栏时不要回滑

我喜欢滑动包,从嵌入你的意见在 UINavigationController继承。不幸的是,我似乎不能找到一种方法来隐藏的 NavigationBar,但仍然有触摸盘滑回 gesture。我可以写自定义手势,但我不喜欢,并依赖于 UINavigationController反向滑动 gesture而不是。

如果我在故事板中取消勾选,后滑动不起作用

enter image description here

或者,如果我通过编程隐藏它,同样的情况。

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

有没有办法隐藏的顶部 NavigationBar仍然有刷卡?

49181 次浏览

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

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.

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;
}

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
}

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

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;

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
}
}

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;
}

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
}
}
}

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.

Problems with Other Methods

Setting the 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 is causing the EXC_BAD_ACCESS errors.

Full Solution

First, add this class to your project:

class InteractivePopRecognizer: NSObject, UIGestureRecognizerDelegate {


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.

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

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

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.

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);
}

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.

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.

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
}
}

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.