web-dev-qa-db-fra.com

Comment pouvez-vous ajouter un UIGestureRecognizer à un UIBarButtonItem comme dans le schéma d'annulation/redo commun UIPopoverController sur les applications iPad?

Problème

Dans mon application iPad, je ne parviens pas à associer une popover à un élément de la barre de boutons uniquement après des événements de maintien et de maintien. Mais cela semble être la norme pour annuler/rétablir. Comment les autres applications font-elles cela?

Contexte

J'ai un bouton d'annulation (UIBarButtonSystemItemUndo) dans la barre d'outils de mon application UIKit (iPad). Lorsque j'appuie sur le bouton d'annulation, il déclenche l'action qui est annulée: et s'exécute correctement.

Cependant, la "convention UE standard" pour l'annulation/la restauration sur iPad est que le fait d'appuyer sur l'annulation exécute une annulation mais le maintenant enfoncé le bouton révèle un contrôleur de popover dans lequel l'utilisateur a sélectionné "annuler" ou "rétablir" jusqu'à ce que le contrôleur est renvoyé.

Le moyen habituel pour attacher un contrôleur popover est avec presentPopoverFromBarButtonItem:, et je peux le configurer assez facilement. Pour que cela ne soit affiché qu'après une pression prolongée, nous devons définir une vue permettant de répondre aux événements de gestes de "pression longue" comme dans l'extrait suivant:

UILongPressGestureRecognizer *longPressOnUndoGesture = [[UILongPressGestureRecognizer alloc] 
       initWithTarget:self 
               action:@selector(handleLongPressOnUndoGesture:)];
//Broken because there is no customView in a UIBarButtonSystemItemUndo item
[self.undoButtonItem.customView addGestureRecognizer:longPressOnUndoGesture];
[longPressOnUndoGesture release];

Après cela, après un appui prolongé sur la vue, la méthode handleLongPressOnUndoGesture: sera appelée, et dans cette méthode, je configurerai et afficherai le popover pour annuler/rétablir. Jusqu'ici tout va bien.

Le problème avec ceci est qu'il y a aucune vue à attacher. self.undoButtonItem est un UIButtonBarItem, pas une vue.

Solutions possibles

1) [L'idéal] attachez la reconnaissance des gestes à l'élément de la barre de boutons. Il est possible d'associer un identificateur de geste à une vue, mais UIButtonBarItem n'est pas une vue. Il a une propriété pour .customView, mais cette propriété est nil lorsque buttonbaritem est un type de système standard (dans ce cas, il l’est).

2) Utilisez une autre vue. Je pourrais utiliser UIToolbar, mais cela nécessiterait des tests de frappe étranges et un piratage complet, si possible dès le départ. Je ne peux penser à aucune autre vue.

3) Utilisez la propriété customView. Les types standard comme UIBarButtonSystemItemUndo n'ont pas de customView (c'est nil). Définir customView effacera le contenu standard dont il a besoin. Cela reviendrait à ré-implémenter tout l’apparence et la fonction de UIBarButtonSystemItemUndo, encore une fois si cela est possible.

Question

Comment puis-je attacher un identificateur de geste à ce "bouton"? Plus spécifiquement, comment puis-je implémenter la fonction presse-maintien-show-redo-popover standard dans une application iPad?

Des idées? Merci beaucoup, surtout si quelqu'un a réellement ce travail dans son application (je pense à vous, omni) et veut partager ...

39
SG.

Remarque: cela ne fonctionne plus à partir de iOS 11

Au lieu d'essayer de trouver la vue UIBarButtonItem dans la liste des sous-vues de la barre d'outils, vous pouvez également essayer ceci, une fois l'élément ajouté à la barre d'outils:

[barButtonItem valueForKey:@"view"];

Ceci utilise la structure de codage Key-Value pour accéder à la variable privée _view d'UIBarButtonItem, où il conserve la vue qu'il a créée.

Certes, je ne sais pas où cela se situe en termes d’API privée d’Apple (c’est une méthode publique utilisée pour accéder à une variable privée d’une classe publique - ce n’est pas comme si on accédait à des frameworks privés pour créer des effets exclusifs d’Apple), mais cela fonctionne, et plutôt sans douleur.

24
Tustin2121

C'est une vieille question, mais cela revient toujours dans les recherches sur Google, et toutes les autres réponses sont trop compliquées.

J'ai une barre de boutons, avec des éléments de barre de boutons, qui appelle une action: forEvent: méthode lorsque vous appuyez dessus.

Dans cette méthode, ajoutez ces lignes:

bool longpress=NO;
UITouch *touch=[[[event allTouches] allObjects] objectAtIndex:0];
if(touch.tapCount==0) longpress=YES;

S'il s'agissait d'un simple toucher, tapCount en est un. Si vous appuyez deux fois, tapCount vaut deux. Si vous appuyez longuement, tapez sur Compteur = zéro.

12
utopian

L'option 1 est en effet possible. Malheureusement, trouver UIView créé par UIBarButtonItem est une opération pénible. Voici comment je l'ai trouvé:

[[[myToolbar subviews] objectAtIndex:[[myToolbar items] indexOfObject:myBarButton]] addGestureRecognizer:myGesture];

C'est plus difficile que cela ne devrait être, mais cela est clairement conçu pour empêcher les gens de jouer avec l'apparence des boutons.

Notez que les espaces fixes/flexibles sont pas comptés comme des vues!

Afin de gérer les espaces, vous devez avoir un moyen de les détecter, et malheureusement, le SDK n’a tout simplement pas le moyen de le faire. Il existe des solutions et en voici quelques unes:

1) Définissez la valeur de la balise UIBarButtonItem sur son index de gauche à droite dans la barre d’outils. Cela nécessite trop de travail manuel pour le maintenir synchronisé IMO.

2) Définissez la propriété activée de tous les espaces sur NO. Utilisez ensuite cet extrait de code pour définir les valeurs de balise pour vous:

NSUInteger index = 0;
for (UIBarButtonItem *anItem in [myToolbar items]) {
    if (anItem.enabled) {
        // For enabled items set a tag.
        anItem.tag = index;
        index ++;
    }
}

// Tag is now equal to subview index.
[[[myToolbar subviews] objectAtIndex:myButton.tag] addGestureRecognizer:myGesture];

Bien sûr, cela présente un piège si vous désactivez un bouton pour une autre raison.

3) Codez manuellement la barre d’outils et gérez vous-même les index. Comme vous allez construire vous-même les UIBarButtonItem, vous saurez ainsi à l'avance quel index ils seront dans les sous-vues. Vous pouvez étendre cette idée à la collecte anticipée d'UIView pour une utilisation ultérieure, si nécessaire.

9
v01d

Au lieu de chercher une sous-vue, vous pouvez créer le bouton vous-même et ajouter un élément de barre de boutons avec une vue personnalisée. Ensuite, vous connectez le GR à votre bouton personnalisé.

7
Ben Stiglitz

Bien que cette question date maintenant de plus d'un an, il s'agit toujours d'un problème assez ennuyant. J'ai soumis un rapport de bogue à Apple ( rdar: // 9982911 ) et je suggère à toute autre personne ayant le même sentiment de le dupliquer.

5
mc.schroeder

J'ai essayé la solution de @ voi1d, qui a très bien fonctionné jusqu'à ce que je modifie le titre du bouton auquel j'ai ajouté un long geste. Changer le titre semble créer un nouvel UIView pour le bouton qui remplace l'original, ce qui empêche le geste ajouté de fonctionner dès qu'une modification est apportée au bouton (ce qui se produit fréquemment dans mon application).

Ma solution consistait à sous-classer UIToolbar et à remplacer la méthode addSubview:. J'ai également créé une propriété qui contient le pointeur sur la cible de mon geste. Voici le code exact:

- (void)addSubview:(UIView *)view {
    // This method is overridden in order to add a long-press gesture recognizer
    // to a UIBarButtonItem. Apple makes this way too difficult, but I am clever!
    [super addSubview:view];
    // NOTE - this depends the button of interest being 150 pixels across (I know...)
    if (view.frame.size.width == 150) {
        UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:targetOfGestureRecognizers 
                                                                                                action:@selector(showChapterMenu:)];
        [view addGestureRecognizer:longPress];
    }
}

Dans ma situation particulière, le bouton qui m'intéresse mesure 150 pixels de diamètre (et c'est le seul bouton qui existe), c'est donc le test que j'utilise. Ce n'est probablement pas le test le plus sûr, mais cela fonctionne pour moi. De toute évidence, vous devrez créer votre propre test et fournir votre propre geste et votre propre sélecteur.

L'avantage de le faire de cette façon est que chaque fois que mon UIBarButtonItem change (et crée ainsi une nouvelle vue), mon geste personnalisé est attaché, de sorte qu'il fonctionne toujours!

2
Michael

Je sais que c'est vieux, mais j'ai passé une nuit à me cogner la tête contre le mur pour essayer de trouver une solution acceptable. Je ne voulais pas utiliser la propriété customView, car cela supprime toutes les fonctionnalités intégrées telles que la teinte des boutons, la teinte désactivée, et la pression longue serait soumise à une zone de résultats aussi réduite, tandis que UIBarButtonItems étendait sa zone de résultats façons. J'ai trouvé cette solution qui, à mon avis, fonctionne vraiment bien et ne demande que peu de mise en œuvre.

Dans mon cas, les 2 premiers boutons de ma barre iraient au même endroit si on appuyait longuement dessus, alors je devais juste détecter qu’une pression s’était produite avant un certain point X. J'ai ajouté la reconnaissance de geste de presse longue à UIToolbar (fonctionne également si vous l'ajoutez à un UINavigationBar), puis j'ai ajouté un UIBarButtonItem supplémentaire de 1 pixel de large juste après le deuxième bouton. Lorsque la vue est chargée, j'ajoute une UIView d'une largeur d'un pixel à UIBarButtonItem car elle est customView. Maintenant, je peux tester le point où la pression a été longue et ensuite voir si X est inférieur au X du cadre de la vue personnalisée. Voici un petit code Swift 3

@IBOutlet var thinSpacer: UIBarButtonItem!

func viewDidLoad() {
    ...
    let thinView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 22))
    self.thinSpacer.customView = thinView

    let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressed(gestureRecognizer:)))
    self.navigationController?.toolbar.addGestureRecognizer(longPress)
    ...
}

func longPressed(gestureRecognizer: UIGestureRecognizer) {
    guard gestureRecognizer.state == .began, let spacer = self.thinSpacer.customView else { return }
    let point = gestureRecognizer.location(ofTouch: 0, in: gestureRecognizer.view)
    if point.x < spacer.frame.Origin.x {
        print("Long Press Success!")
    } else {
        print("Long Pressed Somewhere Else")
    }
}

Certainement pas idéal, mais assez facile pour mon cas d'utilisation. Si vous avez besoin d’appuyer longuement sur des boutons spécifiques à des emplacements spécifiques, cela devient un peu plus gênant, mais vous devriez pouvoir entourer les boutons dont vous avez besoin pour détecter un appui long avec des espaceurs minces, puis vérifiez que le point X de votre point est correct. entre les deux de ces entretoises.

2
Steve E

J'ai essayé quelque chose de similaire à ce que Ben a suggéré. J'ai créé une vue personnalisée avec un UIButton et l'ai utilisée en tant que customView pour UIBarButtonItem. Il y a quelques choses que je n'aimais pas dans cette approche:

  • Le bouton devait être stylé pour ne pas ressortir comme un pouce douloureux sur la barre UIToolBar
  • Avec un UILongPressGestureRecognizer, il ne semblait pas y avoir d’événement de clic pour "Retoucher à l’intérieur" (c’est probablement une erreur de programmation de ma part.)

Au lieu de cela, je me suis contenté de quelque chose de ridicule, mais cela fonctionne pour moi. J'ai utilisé XCode 4.2 et j'utilise ARC dans le code ci-dessous. J'ai créé une nouvelle sous-classe UIViewController appelée CustomBarButtonItemView. Dans le fichier CustomBarButtonItemView.xib, j'ai créé un UIToolBar et ajouté un seul UIBarButtonItem à la barre d'outils. J'ai ensuite réduit la barre d'outils à presque la largeur du bouton. J'ai ensuite connecté la propriété de vue Propriétaire du fichier à UIToolBar.

Interface Builder view of CustomBarButtonViewController

Ensuite, dans le message viewDidLoad de mon ViewController, j'ai créé deux UIGestureRecognizers. Le premier était un UILongPressGestureRecognizer pour le click-and-hold et le deuxième était UITapGestureRecognizer. Je n'arrive pas à obtenir correctement l'action pour UIBarButtonItem dans la vue, alors je la simule avec UITapGestureRecognizer. UIBarButtonItem se présente comme ayant été cliqué et UITapGestureRecognizer prend en charge l'action comme si l'action et la cible de UIBarButtonItem étaient définies.

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib

    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestured)];

    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(buttonPressed:)];

    CustomBarButtomItemView* customBarButtonViewController = [[CustomBarButtomItemView alloc] initWithNibName:@"CustomBarButtonItemView" bundle:nil];

    self.barButtonItem.customView = customBarButtonViewController.view;

    longPress.minimumPressDuration = 1.0;

    [self.barButtonItem.customView addGestureRecognizer:longPress];
    [self.barButtonItem.customView addGestureRecognizer:singleTap];        

}

-(IBAction)buttonPressed:(id)sender{
    NSLog(@"Button Pressed");
};
-(void)longPressGestured{
    NSLog(@"Long Press Gestured");
}

Désormais, lorsqu'un simple clic se produit dans le contrôle barButtonItem du ViewController (connecté via le fichier xib), le geste de tapotement appelle le message buttonPressed:. Si le bouton est maintenu enfoncé, longPressGestured est déclenché.

Pour changer l'apparence de UIBarButton, nous vous suggérons de créer une propriété pour CustomBarButtonItemView afin de permettre l'accès au bouton Bar personnalisé et de le stocker dans la classe ViewController. Lorsque le message longPressGestured est envoyé, vous pouvez modifier l’icône système du bouton.

J'ai trouvé que la propriété customview prend la vue telle quelle. Si vous modifiez le paramètre UIBarButtonitem personnalisé à partir de CustomBarButtonItemView.xib pour modifier le libellé en @ "chaîne vraiment longue" par exemple, le bouton se redimensionnera lui-même mais seule la partie la plus à gauche du bouton affichée se trouve dans la vue surveillée par les instances UIGestuerRecognizer.

2
Joel

Jusqu'à iOS 11, let barbuttonView = barButton.value(forKey: "view") as? UIView nous donnera la référence à la vue pour barButton dans laquelle nous pouvons facilement ajouter des gestes, mais dans iOS 11, les choses sont assez différentes, la ligne de code ci-dessus se retrouvera avec nil donc en ajoutant le geste à la vue pour "vue" clé n'a pas de sens.

Pas de soucis, nous pouvons toujours ajouter des gestes de tapotement aux UIBarItems, car ils ont une propriété customView. Ce que nous pouvons faire est de créer un bouton avec une hauteur et une largeur de 24 pt (conformément aux directives de l’interface utilisateur Apple), puis d’affecter la vue personnalisée au bouton nouvellement créé. Le code ci-dessous vous aidera à effectuer une action pour un tap tap et une autre pour tapoter 5 fois sur le bouton de la barre.

NOTEPour cela, vous devez déjà avoir une référence à l'élément barbutton.

func setupTapGestureForSettingsButton() {
      let multiTapGesture = UITapGestureRecognizer()
      multiTapGesture.numberOfTapsRequired = 5
      multiTapGesture.numberOfTouchesRequired = 1
      multiTapGesture.addTarget(self, action: #selector(HomeTVC.askForPassword))
      let button = UIButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
      button.addTarget(self, action: #selector(changeSettings(_:)), for: .touchUpInside)
      let image = UIImage(named: "test_image")withRenderingMode(.alwaysTemplate)
      button.setImage(image, for: .normal)
      button.tintColor = ColorConstant.Palette.Blue
      settingButton.customView = button
      settingButton.customView?.addGestureRecognizer(multiTapGesture)          
}
2
laxman khanal

Vous pouvez aussi simplement faire ceci ...

let longPress = UILongPressGestureRecognizer(target: self, action: "longPress:")
navigationController?.toolbar.addGestureRecognizer(longPress)

func longPress(sender: UILongPressGestureRecognizer) {
let location = sender.locationInView(navigationController?.toolbar)
println(location)
}
2
Jonny

La 2e option de réponse de @ voi1d est la plus utile pour ceux qui ne souhaitent pas réécrire toutes les fonctionnalités de UIBarButtonItem 's. Je l'ai emballé dans une catégorie pour que vous puissiez simplement faire:

[myToolbar addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton];

avec un peu de gestion des erreurs au cas où vous êtes intéressé. REMARQUE: chaque fois que vous ajoutez ou supprimez des éléments de la barre d’outils à l’aide de setItems, vous devrez rajouter de nouveaux outils de reconnaissance des gestes - j’imagine que UIToolbar recrée les UIViews conservés à chaque fois que vous ajustez le tableau d’éléments.

UIToolbar + Gesture.h

#import <UIKit/UIKit.h>

@interface UIToolbar (Gesture)

- (void)addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton;

@end

UIToolbar + Gesture.m

#import "UIToolbar+Gesture.h"

@implementation UIToolbar (Gesture)

- (void)addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton {
  NSUInteger index = 0;
  NSInteger savedTag = barButton.tag;

  barButton.tag = NSNotFound;
  for (UIBarButtonItem *anItem in [self items]) {
    if (anItem.enabled) {
      anItem.tag = index;
      index ++;
    }
  }
  if (NSNotFound != barButton.tag) {
    [[[self subviews] objectAtIndex:barButton.tag] addGestureRecognizer:recognizer];
  }
  barButton.tag = savedTag;
}

@end
1
natbro

Je sais que ce n'est pas la meilleure solution, mais je vais poster une solution plutôt simple qui a fonctionné pour moi.

J'ai créé une extension simple pour UIBarButtonItem:

fileprivate extension UIBarButtonItem {
    var view: UIView? {
        return value(forKey: "view") as? UIView
    }
    func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
        view?.addGestureRecognizer(gestureRecognizer)
    }
}

Après cela, vous pouvez simplement ajouter vos outils de reconnaissance de mouvements aux éléments de la méthode viewDidLoad de votre ViewController:

@IBOutlet weak var myBarButtonItem: UIBarButtonItem!

func setupLongPressObservation() {
    let recognizer = UILongPressGestureRecognizer(
        target: self, action: #selector(self.didLongPressMyBarButtonItem(recognizer:)))
    myBarButtonItem.addGestureRecognizer(recognizer)
}
0
Kádi

@utopians répond dans Swift 4.2

@objc func myAction(_ sender: UIBarButtonItem, forEvent event:UIEvent) {
    let longPressed:Bool = (event.allTouches?.first?.tapCount).map {$0 == 0} ?? false

    ... handle long press ...
}
0
Klaas

C’est la façon la plus conviviale et la moins astucieuse que j’ai imaginée. Fonctionne sous iOS 12.

Swift 4.2

var longPressTimer: Timer?

let button = UIButton()
button.addTarget(self, action: #selector(touchDown), for: .touchDown)
button.addTarget(self, action: #selector(touchUp), for: .touchUpInside)
button.addTarget(self, action: #selector(touchCancel), for: .touchCancel)
let undoBarButton = UIBarButtonItem(customView: button)

@objc func touchDown() {
    longPressTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(longPressed), userInfo: nil, repeats: false)
}

@objc func touchUp() {
    if longPressTimer?.isValid == false { return } // Long press already activated
    longPressTimer?.invalidate()
    longPressTimer = nil
    // Do tap action
}

@objc func touchCancel() {
    longPressTimer?.invalidate()
    longPressTimer = nil
}

@objc func longPressed() {
    // Do long press action
}
0
Jayden Irwin