web-dev-qa-db-fra.com

Aucun balayage arrière lorsque vous masquez la barre de navigation dans UINavigationController

J'aime le pack de balayage qui a hérité de l'intégration de vos vues dans un UINavigationController. Malheureusement, je n'arrive pas à trouver un moyen de masquer la barre de navigation, mais j'ai toujours le toucher tactile. Je peux écrire des gestes personnalisés, mais je préfère ne pas le faire et me fier plutôt au geste de balayage arrière UINavigationController.

si je décoche le scénario, le glissement de dos ne fonctionne pas

enter image description here

sinon, si je le cache par programme, le même scénario.

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

N'y a-t-il pas moyen de cacher la barre de navigation supérieure tout en ayant le balayage?

65
mihai

Un hack qui fonctionne consiste à définir le délégué interactivePopGestureRecognizer de UINavigationController sur nil comme ceci:

[self.navigationController.interactivePopGestureRecognizer setDelegate:nil];

Mais dans certaines situations, cela pourrait créer des effets étranges.

87
HorseT

Dans mon cas, pour éviter des effets étranges

Contrôleur de vue racine

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/

48
saranpol

Problèmes avec d'autres méthodes

La définition du interactivePopGestureRecognizer.delegate = nil a des effets secondaires inattendus.

La définition de navigationController?.navigationBar.hidden = true fonctionne, mais ne permet pas de masquer vos modifications dans la barre de navigation.

Enfin, il est généralement préférable de créer un objet de modèle qui correspond à UIGestureRecognizerDelegate pour votre contrôleur de navigation. Le paramétrer sur un contrôleur de la pile UINavigationController est à l'origine des erreurs EXC_BAD_ACCESS.

Solution complète

Tout d’abord, ajoutez cette classe à votre projet:

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

Ensuite, définissez le interactivePopGestureRecognizer.delegate de votre contrôleur de navigation sur une instance de votre nouvelle classe InteractivePopRecognizer.

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
}

Profitez d'une barre de navigation cachée sans effets secondaires, qui fonctionne même si votre contrôleur principal a des sous-vues de table, de collection ou de défilement.

47

Vous pouvez sous-classer UINavigationController comme suit:

@interface CustomNavigationController : UINavigationController<UIGestureRecognizerDelegate>

@end

La mise en oeuvre:

@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
16
Yogesh Maheshwari

(Mise à jour) Swift 4.2

J'ai trouvé que d'autres solutions publiées remplaçant le délégué ou le fixant à zéro provoquaient un comportement inattendu. 

Dans mon cas, lorsque je me trouvais au-dessus de la pile de navigation et que j'essayais d’en faire un geste supplémentaire, la tentative échouait (comme prévu), mais les tentatives ultérieures d’application de Pousser sur la pile commençaient à causer d’étranges problèmes graphiques dans la barre de navigation. Cela a du sens, car le délégué est utilisé pour gérer plus que simplement pour empêcher le geste d'être reconnu lorsque la barre de navigation est masquée et que tout ce comportement a été jeté.

D'après mes tests, il apparaît que gestureRecognizer(_:, shouldReceiveTouch:) est la méthode mise en œuvre par le délégué d'origine pour empêcher la reconnaissance du geste lorsque la barre de navigation est masquée, et non pas gestureRecognizerShouldBegin(_:). D'autres solutions qui implémentent gestureRecognizerShouldBegin(_:) dans leur travail de délégué car l'absence d'une implémentation de gestureRecognizer(_:, shouldReceiveTouch:) entraînera le comportement par défaut consistant à recevoir toutes les touches.

La solution de @Nathan Perry se rapproche, mais sans implémentation de respondsToSelector(_:), le code UIKit qui envoie des messages au délégué croira qu'il n'y a pas d'implémentation pour aucune des autres méthodes de délégué et que forwardingTargetForSelector(_:) ne sera jamais appelé.

Nous prenons donc le contrôle de `gestureRecognizer (_ :, shouldReceiveTouch :) dans le scénario spécifique que nous voulons modifier le comportement et sinon transmettons tout le reste au délégué.

import Foundation

class AlwaysPoppableNavigationController: UINavigationController {
    private let alwaysPoppableDelegate = AlwaysPoppableDelegate()

    override func viewDidLoad() {
        super.viewDidLoad()
        alwaysPoppableDelegate.originalDelegate = interactivePopGestureRecognizer?.delegate
        alwaysPoppableDelegate.navigationController = self
        interactivePopGestureRecognizer?.delegate = alwaysPoppableDelegate
    }
}

final class AlwaysPoppableDelegate: NSObject, UIGestureRecognizerDelegate {
    weak var navigationController: UINavigationController?
    weak var originalDelegate: UIGestureRecognizerDelegate?

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

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

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if let nav = navigationController, nav.isNavigationBarHidden, nav.viewControllers.count > 1 {
            return true
        } else if let result = originalDelegate?.gestureRecognizer?(gestureRecognizer, shouldReceive: touch) {
            return result
        } else {
            return false
        }
    }
}
10
Chris Vasselli

À partir de Réponse de Hunter Maximillion Monk , j'ai créé une sous-classe pour UINavigationController, puis défini la classe personnalisée de mon UINavigationController dans mon story-board. Le code final pour les deux classes ressemble à ceci:

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

8
tylermilner

On dirait que la solution fournie par @ChrisVasseli est la meilleure. J'aimerais proposer la même solution en Objective-C car la question concerne Objective-C (voir les balises)

@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
6
Timur Bernikovich

Simple, sans effet secondaire Réponse

Bien que la plupart des réponses données ici soient bonnes, elles ont apparemment des effets secondaires non souhaités (coupure d'application) ou sont prolixes.

La solution la plus simple et fonctionnelle à laquelle je pouvais trouver était la suivante:

Dans le ViewController que vous cachez la barre de navigation,

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

D'autres réponses suggèrent simplement de mettre le délégué à zéro. Si vous faites défiler l'écran vers le contrôleur de vue initial sur la pile de navigation, tous les gestes sont désactivés. Une sorte de surveillance, peut-être, des développeurs de UIKit/UIGesture.

De plus, certaines des réponses que j'ai implémentées ont abouti à un comportement de navigation non standard Apple (permettant en particulier de faire défiler vers le haut ou vers le bas tout en effectuant un balayage arrière). Ces réponses semblent également un peu verbeuses et, dans certains cas, incomplètes.

3
CodyB

Ma solution consiste à étendre directement la classe UINavigationController:

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
    }

}

De cette façon, tous les contrôleurs de navigation seront déplaçables en glissant.

3
fredericdnd

Vous pouvez le faire avec un délégué par procuration. Lorsque vous construisez le contrôleur de navigation, prenez le délégué existant. Et passez-le dans le proxy. Puis passez toutes les méthodes de délégué au délégué existant, à l'exception de gestureRecognizer:shouldReceiveTouch: à l'aide de forwardingTargetForSelector:

Installer:

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

Délégué par procuration:

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
    }  
}
3
Nathan Perry

Xamarin Réponse:

Implémentez l'interface IUIGestureRecognizerDelegate dans la définition de classe de votre ViewController:

public partial class myViewController : UIViewController, IUIGestureRecognizerDelegate

Dans votre ViewController, ajoutez la méthode suivante:

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

Dans ViewDidLoad() de votre ViewController, ajoutez la ligne suivante:

NavigationController.InteractivePopGestureRecognizer.Delegate = this;
1
Ahmad

J'ai essayé cela et cela fonctionne parfaitement: Comment masquer la barre de navigation sans perdre la capacité de glisser en arrière

L'idée est d'implémenter "UIGestureRecognizerDelegate" dans votre fichier .h .__ et de l'ajouter à votre fichier .m.

- (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;  
}
1
KarimIhab

Il y a une solution très simple que j'ai essayée et qui fonctionne parfaitement, c'est dans Xamarin.iOS mais peut être appliquée aussi en natif

    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);
    }
0
João Palma

Voici ma solution: je change alpha dans la barre de navigation, mais la barre de navigation n'est pas masquée. Tous mes contrôleurs de vue sont une sous-classe de mon BaseViewController, et voici:

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

Vous pouvez également sous-classer UINavigationController et y mettre cette méthode.

0

Certaines personnes ont eu du succès en appelant plutôt la méthode setNavigationBarHidden avec YES animée. 

0
Mundi

Dans mon contrôleur de vue sans barre de navigation, j'utilise 

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

Pendant le renvoi interactif, le bouton de retour apparaîtra bien, c'est pourquoi je l'ai caché.

0
fruitcoder