web-dev-qa-db-fra.com

Action de réglage pour le bouton Précédent dans le contrôleur de navigation

J'essaie de remplacer l'action par défaut du bouton de retour dans un contrôleur de navigation. J'ai fourni à une cible une action sur le bouton personnalisé. Ce qui est étrange, c’est que lorsque vous l’attribuez à l’attribut backbutton, il ne leur prête pas attention et affiche la vue actuelle et revient à la racine:

UIBarButtonItem *backButton = [[UIBarButtonItem alloc] 
                                  initWithTitle: @"Servers" 
                                  style:UIBarButtonItemStylePlain 
                                  target:self 
                                  action:@selector(home)];
self.navigationItem.backBarButtonItem = backButton;

Dès que je le mets dans la leftBarButtonItem de la navigationItem il appelle mon action, mais le bouton ressemble à un bouton rond plutôt qu'au bouton fléché en arrière:

self.navigationItem.leftBarButtonItem = backButton;

Comment puis-je le faire appeler mon action personnalisée avant de revenir à la vue racine? Existe-t-il un moyen de remplacer l'action de retour par défaut ou existe-t-il une méthode toujours appelée lorsque vous quittez une vue (viewDidUnload ne le fait pas)?

177
Parrots

Essayez de placer ceci dans le contrôleur de vue où vous voulez détecter la presse:

-(void) viewWillDisappear:(BOOL)animated {
    if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
       // back button was pressed.  We know this is true because self is no longer
       // in the navigation stack.  
    }
    [super viewWillDisappear:animated];
}
363
William Jockusch

J'ai implémenté l'extension IViewController-BackButtonHandler . Il n'a pas besoin de sous-classer quoi que ce soit, il suffit de l'insérer dans votre projet et de remplacer la méthode navigationShouldPopOnBackButton dans UIViewController classe:

-(BOOL) navigationShouldPopOnBackButton {
    if(needsShowConfirmation) {
        // Show confirmation alert
        // ...
        return NO; // Ignore 'Back' button this time
    }
    return YES; // Process 'Back' button click and Pop view controler
}

Télécharger l'exemple d'application .

174
onegray

Contrairement à Amagrammer, c’est possible. Vous devez sous-classer votre navigationController. J'ai tout expliqué ici (avec exemple de code).

42
HansPinckaers

Version rapide:

(de https://stackoverflow.com/a/19132881/826435 )

Dans votre contrôleur de vue, il vous suffit de vous conformer à un protocole et d'effectuer toutes les actions nécessaires:

extension MyViewController: NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress() -> Bool {
        performSomeActionOnThePressOfABackButton()
        return false
    }
}

Puis créez une classe, dites NavigationController+BackButton et copiez-collez le code ci-dessous:

protocol NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress() -> Bool
}

extension UINavigationController {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        // Prevents from a synchronization issue of popping too many navigation items
        // and not enough view controllers or viceversa from unusual tapping
        if viewControllers.count < navigationBar.items!.count {
            return true
        }

        // Check if we have a view controller that wants to respond to being popped
        var shouldPop = true
        if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
            shouldPop = viewController.shouldPopOnBackButtonPress()
        }

        if (shouldPop) {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            // Prevent the back button from staying in an disabled state
            for view in navigationBar.subviews {
                if view.alpha < 1.0 {
                    UIView.animate(withDuration: 0.25, animations: {
                        view.alpha = 1.0
                    })
                }
            }

        }

        return false
    }
}
13
kgaidis

Pour des raisons de threading, la solution mentionnée par @HansPinckaers ne me convenait pas, mais j'ai trouvé un moyen très simple d'attaquer le bouton du dos, et je tiens à le préciser ici au cas où cela permettrait d'éviter des heures de tromperies pour quelqu'un d'autre. Le truc est vraiment simple: ajoutez simplement un UIButton transparent en tant que sous-vue à votre UINavigationBar, et définissez vos sélecteurs pour lui comme si c’était le vrai bouton! Voici un exemple utilisant Monotouch et C #, mais la traduction en objective-c ne devrait pas être trop difficile à trouver.

public class Test : UIViewController {
    public override void ViewDidLoad() {
        UIButton b = new UIButton(new RectangleF(0, 0, 60, 44)); //width must be adapted to label contained in button
        b.BackgroundColor = UIColor.Clear; //making the background invisible
        b.Title = string.Empty; // and no need to write anything
        b.TouchDown += delegate {
            Console.WriteLine("caught!");
            if (true) // check what you want here
                NavigationController.PopViewControllerAnimated(true); // and then we pop if we want
        };
        NavigationController.NavigationBar.AddSubview(button); // insert the button to the nav bar
    }
}

Anecdote: à des fins de test et pour trouver de bonnes dimensions pour mon faux bouton, j'ai défini la couleur d'arrière-plan sur bleu ... Et cela indique derrière le bouton de retour! Quoi qu'il en soit, il capture toujours le moindre contact avec le bouton d'origine.

5
psycho

Ce n'est pas possible de faire directement. Il y a quelques alternatives:

  1. Créez votre propre UIBarButtonItem personnalisé qui valide au robinet et apparaît si le test réussit
  2. Validez le contenu du champ de formulaire à l'aide d'une méthode déléguée UITextField, telle que -textFieldShouldReturn: , appelée après avoir appuyé sur le bouton Return ou Done le clavier

L'inconvénient de la première option est que le style de flèche vers la gauche du bouton Précédent n'est pas accessible à partir d'un bouton de barre personnalisé. Donc, vous devez utiliser une image ou aller avec un bouton de style régulier.

La deuxième option est Nice car vous récupérez le champ de texte dans la méthode déléguée, vous pouvez ainsi cibler votre logique de validation sur le champ de texte spécifique envoyé à la méthode de rappel par délégué.

5
Alex Reynolds

Cette technique vous permet de modifier le texte du bouton "Précédent" sans affecter le titre des contrôleurs de vue ni de voir le texte du bouton Précédent changer pendant l'animation.

Ajoutez ceci à la méthode init dans le contrôleur de vue appelant :

UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];   
temporaryBarButtonItem.title = @"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
3
Jason Moore

Nous avons trouvé une solution qui conserve également le style du bouton de retour. Ajoutez la méthode suivante à votre contrôleur de vue.

-(void) overrideBack{

    UIButton *transparentButton = [[UIButton alloc] init];
    [transparentButton setFrame:CGRectMake(0,0, 50, 40)];
    [transparentButton setBackgroundColor:[UIColor clearColor]];
    [transparentButton addTarget:self action:@selector(backAction:) forControlEvents:UIControlEventTouchUpInside];
    [self.navigationController.navigationBar addSubview:transparentButton];


}

Maintenant, fournissez une fonctionnalité selon vos besoins dans la méthode suivante:

-(void)backAction:(UIBarButtonItem *)sender {
    //Your functionality
}

Tout ce qu'il fait est de couvrir le bouton de retour avec un bouton transparent;)

3
Sarasranglt

moyen le plus simple

Vous pouvez utiliser les méthodes de délégué de UINavigationController. La méthode willShowViewController est appelée lorsque vous appuyez sur le bouton Précédent de votre VC.

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
3
Xar E Ahmer

Il existe un moyen plus simple de simplement sous-classer la méthode déléguée de la UINavigationBar et substitution la ShouldPopItemméthode.

2
jazzyjef2002

Voici la version de Swift 3 de @ oneway pour intercepter l'événement du bouton de retour de la barre de navigation avant qu'il ne soit déclenché. Comme UINavigationBarDelegate ne peut pas être utilisé pour UIViewController, vous devez créer un délégué qui sera déclenché lors de l'appel de navigationBarshouldPop.

@objc public protocol BackButtonDelegate {
      @objc optional func navigationShouldPopOnBackButton() -> Bool 
}

extension UINavigationController: UINavigationBarDelegate  {

    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

        if viewControllers.count < (navigationBar.items?.count)! {                
            return true
        }

        var shouldPop = true
        let vc = self.topViewController

        if vc.responds(to: #selector(vc.navigationShouldPopOnBackButton)) {
            shouldPop = vc.navigationShouldPopOnBackButton()
        }

        if shouldPop {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            for subView in navigationBar.subviews {
                if(0 < subView.alpha && subView.alpha < 1) {
                    UIView.animate(withDuration: 0.25, animations: {
                        subView.alpha = 1
                    })
                }
            }
        }

        return false
    }
}

Et ensuite, dans votre contrôleur de vue, ajoutez la fonction de délégué:

class BaseVC: UIViewController, BackButtonDelegate {
    func navigationShouldPopOnBackButton() -> Bool {
        if ... {
            return true
        } else {
            return false
        }        
    }
}

J'ai réalisé que nous voulions souvent ajouter un contrôleur d'alerte pour que les utilisateurs décident s'ils souhaitent revenir en arrière. Si tel est le cas, vous pouvez toujours return false dans navigationShouldPopOnBackButton() fonctionner et fermer votre contrôleur de vue en procédant comme suit:

func navigationShouldPopOnBackButton() -> Bool {
     let alert = UIAlertController(title: "Warning",
                                          message: "Do you want to quit?",
                                          preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { UIAlertAction in self.yes()}))
            alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { UIAlertAction in self.no()}))
            present(alert, animated: true, completion: nil)
      return false
}

func yes() {
     print("yes")
     DispatchQueue.main.async {
            _ = self.navigationController?.popViewController(animated: true)
        }
}

func no() {
    print("no")       
}
2
Lawliet

Je ne crois pas que ce soit possible, facilement. La seule façon pour moi de contourner ce problème est de créer votre propre image de flèche de bouton arrière à placer là-haut. C'était frustrant pour moi au début, mais je vois pourquoi, pour des raisons de cohérence, il a été laissé de côté.

Vous pouvez vous en approcher (sans la flèche) en créant un bouton régulier et en masquant le bouton de retour par défaut:

self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
2
Meltemi

Utiliser Swift:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
2
Murray Sagal

selon les documents officiels d'Apple, https://developer.Apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClisting.html , nous devrait éviter de le faire.

"Si le nom d'une méthode déclarée dans une catégorie est identique à une méthode de la classe d'origine ou à une autre catégorie de la même classe (ou même une superclasse), le comportement n'est pas défini quant à la méthode utilisée Cela risque moins de poser problème si vous utilisez des catégories avec vos propres classes, mais peut entraîner des problèmes lorsque vous utilisez des catégories pour ajouter des méthodes aux classes standard Cocoa ou Cocoa Touch. "

2
user2612791

Voici ma Swift solution. Dans votre sous-classe de UIViewController, substituez la méthode navigationShouldPopOnBackButton.

extension UIViewController {
    func navigationShouldPopOnBackButton() -> Bool {
        return true
    }
}

extension UINavigationController {

    func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
        if let vc = self.topViewController {
            if vc.navigationShouldPopOnBackButton() {
                self.popViewControllerAnimated(true)
            } else {
                for it in navigationBar.subviews {
                    let view = it as! UIView
                    if view.alpha < 1.0 {
                        [UIView .animateWithDuration(0.25, animations: { () -> Void in
                            view.alpha = 1.0
                        })]
                    }
                }
                return false
            }
        }
        return true
    }

}
2
AutomatonTec

La réponse de @William est correcte. Cependant, si l'utilisateur lance un geste de balayage, la méthode viewWillDisappear est appelée et même self ne sera pas dans la pile de navigation (c'est-à-dire, self.navigationController.viewControllers ne contiendra pas self), même si le balayage n'est pas terminé et que le contrôleur de vue n'est pas affiché. Ainsi, la solution serait de:

  1. Désactivez le geste de balayage dans viewDidAppear et n'utilisez que le bouton Précédent en utilisant:

    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)])
    {
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
    
  2. Ou utilisez simplement viewDidDisappear à la place, comme suit:

    - (void)viewDidDisappear:(BOOL)animated
    {
        [super viewDidDisappear:animated];
        if (![self.navigationController.viewControllers containsObject:self])
        {
            // back button was pressed or the the swipe-to-go-back gesture was
            // completed. We know this is true because self is no longer
            // in the navigation stack.
        }
    }
    
1
boherna

Cette approche a fonctionné pour moi (mais le bouton "Précédent" n'aura pas le signe "<"):

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIBarButtonItem* backNavButton = [[UIBarButtonItem alloc] initWithTitle:@"Back"
                                                                      style:UIBarButtonItemStyleBordered
                                                                     target:self
                                                                     action:@selector(backButtonClicked)];
    self.navigationItem.leftBarButtonItem = backNavButton;
}

-(void)backButtonClicked
{
    // Do something...
    AppDelegate* delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
    [delegate.navController popViewControllerAnimated:YES];
}
1
Ivan

Utilisez isMovingFromParentViewController

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

    if self.isMovingFromParentViewController {
        // current viewController is removed from parent
        // do some work
    }
}
1
herrk

Pour un formulaire qui nécessite une telle saisie de la part de l'utilisateur, je vous recommande de l'invoquer en tant que "modal" au lieu de faire partie de votre pile de navigation. De cette façon, ils doivent s'occuper des affaires sur le formulaire, vous pouvez ensuite le valider et le rejeter à l'aide d'un bouton personnalisé. Vous pouvez même concevoir une barre de navigation qui ressemble au reste de votre application mais vous donne plus de contrôle.

1
Travis M.

Vous pouvez essayer d'accéder à l'élément Bouton droit de NavigationBars et définir sa propriété de sélecteur ... voici une référence Une référence UIBarButtonItem , une autre chose si cela fonctionne travail est, définissez le bouton droit de la barre de navigation sur un UIBarButtonItem personnalisé que vous créez et définissez son sélecteur ... espérons que cela aide

1
Daniel

Pour intercepter le bouton Précédent, recouvrez-le simplement d'un UIControl transparent et interceptez les touches.

@interface MyViewController : UIViewController
{
    UIControl   *backCover;
    BOOL        inhibitBackButtonBOOL;
}
@end

@implementation MyViewController
-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    // Cover the back button (cannot do this in viewWillAppear -- too soon)
    if ( backCover == nil ) {
        backCover = [[UIControl alloc] initWithFrame:CGRectMake( 0, 0, 80, 44)];
#if TARGET_IPHONE_SIMULATOR
        // show the cover for testing
        backCover.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.15];
#endif
        [backCover addTarget:self action:@selector(backCoverAction) forControlEvents:UIControlEventTouchDown];
        UINavigationBar *navBar = self.navigationController.navigationBar;
        [navBar addSubview:backCover];
    }
}

-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    [backCover removeFromSuperview];
    backCover = nil;
}

- (void)backCoverAction
{
    if ( inhibitBackButtonBOOL ) {
        NSLog(@"Back button aborted");
        // notify the user why...
    } else {
        [self.navigationController popViewControllerAnimated:YES]; // "Back"
    }
}
@end
1
Jeff

Swift 4 iOS 11.3 Version:

Ceci s'appuie sur la réponse de kgaidis de https://stackoverflow.com/a/34343418/4316579

Je ne sais pas quand l'extension a cessé de fonctionner, mais au moment d'écrire ces lignes (Swift 4), il semble que l'extension ne sera plus exécutée à moins que vous ne déclariez la conformité UINavigationBarDelegate comme décrit ci-dessous.

J'espère que cela aidera les gens qui se demandent pourquoi leur extension ne fonctionne plus.

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

    }
}
1
Edward L.

Au moins dans Xcode 5, il existe une solution simple et plutôt bonne (pas parfaite). Dans IB, faites glisser un élément de bouton de barre du volet Utilitaires et déposez-le à gauche de la barre de navigation à l'emplacement du bouton Précédent. Définissez l'étiquette sur "Back". Vous aurez un bouton fonctionnel que vous pourrez lier à votre IBAction et fermer votre viewController. Je travaille, puis je déclenche une transition et tout fonctionne parfaitement.

Ce qui n’est pas idéal, c’est que ce bouton n’obtienne pas la flèche <et ne reporte pas le titre précédent des VC, mais je pense que cela peut être géré. Pour mes besoins, j'ai défini le nouveau bouton Précédent comme étant un bouton "Terminé" afin que son objectif soit clair.

Vous vous retrouvez également avec deux boutons Précédent dans le navigateur IB, mais il est assez facile de le nommer par souci de clarté.

enter image description here

1
Dan Loughney

En utilisant les variables cible et d'action que vous quittez 'actuellement', vous devriez pouvoir connecter vos dialogues de sauvegarde afin qu'ils soient appelés lorsque le bouton est "sélectionné". Attention, cela peut se déclencher à des moments étranges.

Je suis principalement d'accord avec Amagrammer, mais je ne pense pas qu'il serait difficile de créer le bouton avec la flèche personnalisée. Je voudrais juste renommer le bouton de retour, prendre une capture d’écran, Photoshop la taille du bouton nécessaire, et que ce soit l’image en haut de votre bouton.

1
TahoeWolverine

Swift

override func viewWillDisappear(animated: Bool) {
    let viewControllers = self.navigationController?.viewControllers!
    if indexOfArray(viewControllers!, searchObject: self) == nil {
        // do something
    }
    super.viewWillDisappear(animated)
}

func indexOfArray(array:[AnyObject], searchObject: AnyObject)-> Int? {
    for (index, value) in enumerate(array) {
        if value as UIViewController == searchObject as UIViewController {
            return index
        }
    }
    return nil
}
1
zono

Trouvé une nouvelle façon de le faire:

Objectif c

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == NULL) {
        NSLog(@"Back Pressed");
    }
}

Swift

override func didMoveToParentViewController(parent: UIViewController?) {
    if parent == nil {
        println("Back Pressed")
    }
}
0
Ashish Kakkad

La solution que j'ai trouvée jusqu'à présent n'est pas très gentille, mais ça marche pour moi. En prenant ceci réponse , je vérifie également si je saute par programmation ou non:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if ((self.isMovingFromParentViewController || self.isBeingDismissed)
      && !self.isPoppingProgrammatically) {
    // Do your stuff here
  }
}

Vous devez ajouter cette propriété à votre contrôleur et lui attribuer la valeur YES avant d'afficher le programme par programmation:

self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];
0
Ferran Maylinch

Construire sur les réponses précédentes avec IAlert dans Swift5 de manière asynchrone


protocol NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress(_ completion: @escaping (Bool) -> ())
}

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

        if viewControllers.count < navigationBar.items!.count {
            return true
        }

        // Check if we have a view controller that wants to respond to being popped

        if let viewController = topViewController as? NavigationControllerBackButtonDelegate {

            viewController.shouldPopOnBackButtonPress { shouldPop in
                if (shouldPop) {
                    /// on confirm => pop
                    DispatchQueue.main.async {
                        self.popViewController(animated: true)
                    }
                } else {
                    /// on cancel => do nothing
                }
            }
            /// return false => so navigator will cancel the popBack
            /// until user confirm or cancel
            return false
        }else{
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        }
        return true
    }
}


sur votre contrôleur


extension MyController: NavigationControllerBackButtonDelegate {

    func shouldPopOnBackButtonPress(_ completion: @escaping (Bool) -> ()) {

        let msg = "message"

        /// show UIAlert
        alertAttention(msg: msg, actions: [

            .init(title: "Continuer", style: .destructive, handler: { _ in
                completion(true)
            }),
            .init(title: "Annuler", style: .cancel, handler: { _ in
                completion(false)
            })
            ])

    }

}
0
brahimm

version rapide de la réponse de @ onegray

protocol RequestsNavigationPopVerification {
    var confirmationTitle: String { get }
    var confirmationMessage: String { get }
}

extension RequestsNavigationPopVerification where Self: UIViewController {
    var confirmationTitle: String {
        return "Go back?"
    }

    var confirmationMessage: String {
        return "Are you sure?"
    }
}

final class NavigationController: UINavigationController {

    func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {

        guard let requestsPopConfirm = topViewController as? RequestsNavigationPopVerification else {
            popViewControllerAnimated(true)
            return true
        }

        let alertController = UIAlertController(title: requestsPopConfirm.confirmationTitle, message: requestsPopConfirm.confirmationMessage, preferredStyle: .Alert)

        alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel) { _ in
            dispatch_async(dispatch_get_main_queue(), {
                let dimmed = navigationBar.subviews.flatMap { $0.alpha < 1 ? $0 : nil }
                UIView.animateWithDuration(0.25) {
                    dimmed.forEach { $0.alpha = 1 }
                }
            })
            return
        })

        alertController.addAction(UIAlertAction(title: "Go back", style: .Default) { _ in
            dispatch_async(dispatch_get_main_queue(), {
                self.popViewControllerAnimated(true)
            })
        })

        presentViewController(alertController, animated: true, completion: nil)

        return false
    }
}

Maintenant, dans n'importe quel contrôleur, conformez-vous à RequestsNavigationPopVerification et ce comportement est adopté par défaut.

0
Adam Waite