No Swipe Back when hiding Navigation Bar in UINavi

2019-01-12 17:16发布

问题:

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 navigation bar 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

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 Navigation bar and still have the swipe?

回答1:

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.



回答2:

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 

}


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

http://www.gampood.com/pop-viewcontroller-with-out-navigation-bar/



回答3:

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.



回答4:

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


回答5:

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.

import Foundation

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?
    var originalDelegate: UIGestureRecognizerDelegate

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

    @objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if let nav = navigationController, nav.isNavigationBarHidden && nav.viewControllers.count > 1 { {
            return true
        }
        else {
            return self.originalDelegate.gestureRecognizer!(gestureRecognizer, shouldReceive: touch)
        }
    }

    override func responds(to aSelector: Selector) -> Bool {
        if aSelector == #selector(gestureRecognizer(_:shouldReceive:)) {
            return true
        }
        else {
            return self.originalDelegate.responds(to: aSelector)
        }
    }

    override func forwardingTarget(for aSelector: Selector!) -> Any? {
        return self.originalDelegate
    }

}


回答6:

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:



回答7:

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


回答8:

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


回答9:

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;


回答10:

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


回答11:

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.



回答12:

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



回答13:

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.



回答14:

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


回答15:

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