web-dev-qa-db-fra.com

Problèmes avec la reconnaissance des gestes dans iOS 7

J'ajoute plusieurs UIView objets (par exemple 5) à l'écran, l'un dans l'autre. Ceci, par exemple, view5.superview = view4, view4.superview = view3, view3.superview=view2, view2.superview = view1. Pour tous ces UIView j'ai mis uitapgesturerecognizer; pour view1-4 je fais juste NSLog (@ "tap% @", self) en rappel, tandis que pour view5 tap je règle ce qui suit: supprimer view4 de la hiérarchie, puis mettre le même objet view4 'au même endroit de la hiérarchie . Cet objet contient également view5 'pour lequel UITapGestureRecognizer est défini (pratiquement, je remplace une partie du balisage par une similaire).

Ensuite, je commence à cliquer sur view5. Un certain temps, view5 continue d'attraper son tap et tout va bien, mais un nombre aléatoire de taps plus tard (chaque fois que ce nombre est différent) l'un des objets view1-4 commence à attraper ce tap, bien que nous cliquions toujours sur la vue5. L'ensemble du problème a un caractère aléatoire - parfois il se produit au 10e lancement, parfois au second. Parfois, des objets incorrects commencent à attraper des robinets au premier tapotement. De plus, je ne sais jamais quel objet va attraper un robinet, quand tout va mal. Le cadre pour la vue (n + 1) a été défini, par exemple, comme la moitié de la vue du cadre (n), tandis que le cadre pour la vue 1 - par exemple (0,0 320, 460).

Toutes les opérations avec les objets ui décrits ci-dessus sont effectuées dans le thread principal, et tout ce dont j'ai parlé fonctionnait parfaitement sur iOS 4.3 - 6.1 avec des exemples beaucoup plus complexes. Mais l'iOS7 en fait une sorte d'enfer aléatoire.

Mise à jour: J'ai créé un exemple de projet, pour simplifier le processus de débogage. Aucune opération d'ajout/suppression de sous-vues sur le robinet. Seulement 4 vues à l'écran, appuyez sur les journaux de l'application quelle vue a été tapée. Vous devez donc appuyer sur la plus petite vue (4). Si vous voyez "tap 4 tap 4 tap 4…" dans le journal - c'est le cas quand tout fonctionne bien, arrêtez et courez à nouveau, arrêtez et courez à nouveau, arrêtez et courez à nouveau, etc. Et à certains runs (peut-être après 10 + runs réussis) vous ne verrez pas "tap 4" sur la première ligne, vous verrez "tap 1" ou "tap 2" ou "tap 3", et cela continuera ainsi - ce sont les mauvais cas.

Un exemple de projet peut être téléchargé à partir d'ici: http://tech.octopod.com/test/BuggySample.Zip (seulement 33 Kb dans les archives).

Mise à jour 2

Nous avons publié un bug sur Apple, je posterai ici lorsque nous aurons des commentaires. Cependant, toute bonne solution de contournement serait très appréciée!

Mise à jour 3

La solution, fournie par Yuvrajsinh, fonctionne vraiment sur l'exemple de projet. Malheureusement, cela n'aide toujours pas à résoudre le problème survenu dans le projet principal où il est apparu initialement. La principale raison pour l'instant est que si une vue sans auto-geste repose sur le contenu cliquable, l'élément de vue aléatoire en dessous commence à capturer l'interaction (au lieu de celui du haut avec le jeu de gestes d'interaction. Avez-vous des idées sur la façon de le résoudre ? L'échantillon mis à jour peut être téléchargé à partir d'ici: http://tech.octopod.com/test/BuggySample2.Zip

25
Kup

Étant donné que le problème se produit uniquement dans iOS 7, vous pouvez utiliser l'une des nouvelles méthodes de délégué pour résoudre le problème:

– gestureRecognizer:shouldRequireFailureOfGestureRecognizer:
– gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:

Je l'ai résolu en implémentant gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer Et en "explorant" la vue d'ensemble de la vue du geste afin que je puisse retourner "OUI" si je trouve que le geste de la vue d'ensemble est égal à celui fourni. Je détaille ma résolution complète ici: https://stackoverflow.com/a/19659848/1147934 .

Explication
Le problème avec les reconnaisseurs de gestes dans iOS 7 est que le geste d'un superview reçoit ses touches avant qu'un de ses gestes de sous-vue ne le reçoive. Cela provoque la reconnaissance du geste de superview qui annule ensuite le reconnaisseur de la vue secondaire ... c'est (incorrect?) Et plusieurs bogues ont été déposés auprès d'Apple. Il a été souligné que Apple ne garantit pas l'ordre dans lequel les gestes sont touchés. Je pense que beaucoup de "nous" comptons sur un détail d'implémentation qui a changé dans iOS 7. C'est pourquoi nous utilisons les nouvelles méthodes déléguées, qui semblent conçues pour nous aider à résoudre ce problème.

Remarque: J'ai effectué des tests approfondis en utilisant mes propres outils de reconnaissance de sous-classe, en enregistrant toutes les touches et j'ai découvert que la raison pour laquelle les modules de reconnaissance échouent est que les gestes de vue d'ensemble recevaient des touches avant une le geste de subview était dans environ 5% des cas. Chaque fois que cela s'est produit, un échec s'est produit. Cela se produit plus souvent si vous avez des hiérarchies "profondes" avec beaucoup de gestes.

Les nouvelles méthodes de délégué peuvent être déroutantes, vous devez donc les lire attentivement.

J'utilise la méthode (j'ai renommé les arguments pour les rendre plus faciles à comprendre)

– gestureRecognizer:(UIGestureRecognizer *)thisRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *) otherRecognizer.

Si vous retournez "OUI", la reconnaissance de mouvement fournie, otherRecognizer, nécessitera thisRecognizer pour échouer avant de pouvoir être reconnue. C'est pourquoi, dans ma réponse, j'explore la hiérarchie de superview pour vérifier si elle contient une superview qui a le otherRecognizer. Si c'est le cas, je veux que otherRecognizer exige que thisRecognizer échoue parce que thisRecognizer est dans une sous-vue et devrait échouer avant d'être le geste de superview est reconnu. Cela garantira que les gestes de sous-vue sont reconnus avant les gestes de leur vue d'ensemble. Ça a du sens?

Alternative
Je pourrais m'y prendre dans l'autre sens et utiliser:

– gestureRecognizer:(UIGestureRecognizer *)thisRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherRecognizer

Maintenant, j'aurais besoin d'explorer toute ma hiérarchie de sous-vue , de vérifier si otherRecognizer est dedans et de retourner YES si c'est le cas. Je n'utilise pas cette méthode, car l'exploration de la hiérarchie de sous-vue entière est beaucoup plus difficile et coûteuse à faire que de vérifier une hiérarchie de vue d'ensemble. L'analyse d'une hiérarchie de sous-vues devrait être une fonction récursive, tandis que je peux utiliser une simple boucle while pour vérifier la hiérarchie d'une vue d'ensemble. Je recommande donc la première approche que je décris.

Attention!
Soyez prudent lorsque vous utilisez gestureRecognizer:shouldReceiveTouch:. Le problème est un problème dont le geste reçoit le toucher en premier (annulation de l'autre geste) ... c'est un problème de résolution de conflit. Si vous implémentez gestureRecognizer:shouldReceiveTouch:, Vous risquez de rejeter le geste d'une vue d'ensemble si le geste de sous-vue échoue parce que vous devez deviner quand un geste de sous-vue pourrait être reconnu. Un geste de sous-vue peut légitimement échouer pour des raisons autres que les touches sont hors limites, vous devez donc connaître les détails de mise en œuvre afin de deviner correctement. Vous voulez que le geste de vue d'ensemble soit reconnu lorsque le geste de sous-vue échoue, mais vous n'avez pas vraiment de toute façon savoir avec certitude s'il échouera avant qu'il n'échoue réellement. Si un geste de sous-vue échoue, vous voulez normalement que le geste de vue d'ensemble reconnaisse ensuite. Il s'agit de la chaîne de réponse normale (vue d'ensemble de la sous-vue) et si vous vous trompez, vous pourriez vous retrouver avec un comportement inattendu.

19
Aaron Hayman

J'ai apporté quelques modifications à votre code et je l'ai également beaucoup testé et le problème ne se produit pas.

Lors de la création de la vue, j'ai défini une balise sur chaque vue pour la distinguer:

View1234 *v1 = [[View1234 alloc] initWithId:@"1"];
v1.tag =1;
v1.backgroundColor = [UIColor redColor];
v1.frame = CGRectMake(0, 0, 320, 460);

View1234 *v2 = [[View1234 alloc] initWithId:@"2"];
v2.tag=2;
v2.backgroundColor = [UIColor greenColor];
v2.frame = CGRectMake(0, 0, 160, 230);

View1234 *v3 = [[View1234 alloc] initWithId:@"3"];
v3.tag=3;
v3.backgroundColor = [UIColor blueColor];
v3.frame = CGRectMake(0, 0, 80, 115);

View1234 *v4 = [[View1234 alloc] initWithId:@"4"];
v4.tag=4;
v4.backgroundColor = [UIColor orangeColor];
v4.frame = CGRectMake(0, 0, 40, 50);

View1234.h

@interface View1234 : UIView <UIGestureRecognizerDelegate> {
    NSString *vid;
}

- (id) initWithId:(NSString *)vid_;
@end

Et voici tout le code de View1234.m

- (id) initWithId:(NSString *)vid_ {
    if (self = [super init]) {

        vid = [vid_ retain];

        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] init];
        tapGesture.delegate = self;
        tapGesture.numberOfTapsRequired = 1;
        tapGesture.numberOfTouchesRequired = 1;
        [tapGesture addTarget:self action:@selector(handleTap:)];

        [self addGestureRecognizer:tapGesture];

        [tapGesture release];

    }

    return self;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
    if (touch.view==self ) {
        return YES;
    }
    else if ([self.subviews containsObject:touch.view] && ![touch.view isKindOfClass:[self class]])
    {
        return YES;
    }
    return NO;
}

/*- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{

    if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
        if (self.tag==gestureRecognizer.view.tag) {
            return YES;
        }
    }

    return NO;
}*/

- (void) handleTap:(UITapGestureRecognizer *)tap {
    NSLog(@"tap %@", vid);
}

- (void) dealloc {

    [vid release];
    vid = nil;

    [super dealloc];
}

MISE À JOUR : Pourquoi ce problème survient réellement.

Lorsque vous ajoutez un UIView en tant que sous-vue d'un autre UIView avec UITapGestureRecognizer dans chaque vue, puis dans certains cas rares UITapGestureRecognizer l'état devient Échec en quelque sorte ( Je l'ai débogué plus de 50 fois et j'ai appris cela ). Ainsi, lorsqu'une sous-vue d'une vue n'est pas en mesure de gérer le geste de prise, le système passera le geste à sa super vue pour gérer ce geste, et cela continue.

Si vous déboguez, vous saurez que gestureRecognizerShouldBegin est appelé plusieurs fois selon la hiérarchie de vue. Dans ce cas particulier, si je tape sur view3 puis gestureRecognizerShouldBegin appellera fois comme view3 est au 3ème niveau de la hiérarchie des vues, donc gestureRecognizerShouldBegin sera appelé pour view3, view2 and view1.

Donc, pour résoudre le problème, je renvoie YES formulaire gestureRecognizerShouldBegin pour la vue correcte et NO pour le reste, donc cela résout le problème.

MISE À JOUR 2: J'ai apporté des modifications au code dans ma réponse modifiée et j'espère que cela résoudra votre problème. Et aussi grâce à @masmor, j'ai également trouvé des indices de sa réponse pour résoudre le problème.

12
Yuvrajsinh

Définissez un délégué sur le module de reconnaissance et implémentez gestureRecognizer:shouldReceiveTouch:.

L'implémentation devrait fondamentalement bloquer les contacts sur les sous-vues, mais il y aura probablement des critères supplémentaires en fonction de votre hiérarchie et de votre configuration de vue réelles.

L'exemple ci-dessous vérifie simplement si la vue de hit est une sous-vue directe et si elle a des reconnaisseurs de gestes, auquel cas il est autorisé à voir les touches.

Les touches de blocage devraient être plus robustes que de jouer avec les états de reconnaissance, car il n'y a aucune chance pour que des vues indésirables déclenchent leurs reconnaisseurs. La nécessité d'implémenter des critères personnalisés est un inconvénient, mais encore une fois, je pense qu'il est plus robuste d'implémenter explicitement un comportement lorsque vous contournez un bogue de cause inconnue comme dans ce cas.

#import "View1234.h"

@interface View1234 () <UIGestureRecognizerDelegate>

@end

@implementation View1234

- (id) initWithId:(NSString *)vid_ {
    if (self = [super init]) {

        vid = [vid_ retain];

        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] init];
        tapGesture.numberOfTapsRequired = 1;
        tapGesture.numberOfTouchesRequired = 1;
        [tapGesture addTarget:self action:@selector(handleTap:)];

        tapGesture.delegate = self;

        [self addGestureRecognizer:tapGesture];

        [tapGesture release];
    }

    return self;
}

//- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
//    
//    if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
//        if (self.tag==gestureRecognizer.view.tag) {
//            return YES;
//        }
//    }
//    
//    return NO;
//}

- (void) handleTap:(id)tap {
    NSLog(@"tap %@", vid);
}

- (void) dealloc {

    [vid release];
    vid = nil;

    [super dealloc];
}

- (BOOL)shouldReceiveTouchOnView:(UIView *)hitView {
  NSLog(@"Should view:\n%@\nreceive touch on view:\n%@", self, hitView);

  // Replace this implementation with whatever you need...
  // Here, we simply check if the view has a gesture recognizer and
  // is a direct subview.
  BOOL res = (hitView.gestureRecognizers.count == 0 &&
              [self.subviews containsObject:hitView]);

  NSLog(@"%@", res? @"YES":@"NO");

  return res;
}

#pragma mark - Gesture Recognizer Delegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
       shouldReceiveTouch:(UITouch *)touch {
  UIView *hitView = [self hitTest:[touch locationInView:self.superview]
                        withEvent:nil];
  if (hitView == self) {
    NSLog(@"Touch not in subview");
    return YES;
  }

  return [self shouldReceiveTouchOnView:hitView];
}

@end
2
voxlet

Je n'ai pas essayé votre projet ou ci-dessous.

Vous devriez pouvoir utiliser gestureRecognizerShouldBegin: pour empêcher tout geste n'appartenant pas à la vue de se déclencher lorsque la vue est touchée.

Vous pouvez le faire avec une sous-classe de UIView, ou vous pouvez créer une catégorie sur UIView (avec une propriété ou un objet associé) qui ajoute un indicateur pour déterminer ce que chaque instance de vue doit faire - cela va casser certains types de vues, donc méfiez-vous.

Cela n'aidera pas si le problème est l'ordre des vues ...

0
Wain