102 votes

Capture des contacts sur une sous-vue en dehors du cadre de sa vue supérieure à l'aide de hitTest:withEvent :

Mon problème : J'ai une vue supérieure EditView qui occupe pratiquement tout le cadre de l'application, et une sous-vue MenuView qui n'occupe que les ~20% du bas, et ensuite MenuView contient sa propre sous-vue ButtonView qui réside en fait en dehors de MenuView (quelque chose comme ceci : ButtonView.frame.origin.y = -100 ).

(note : EditView possède d'autres sous-vues qui ne font pas partie de l'ensemble de l'UE. MenuView de la hiérarchie des vues, mais peut affecter la réponse).

Vous connaissez probablement déjà le problème : lorsque ButtonView est dans les limites de MenuView (ou, plus précisément, lorsque mes touches sont à l'intérieur de MenuView ), ButtonView réagit aux événements liés au toucher. Lorsque mes touchers sont en dehors de MenuView (mais toujours dans les limites de la ButtonView ), aucun événement de contact n'est reçu par l'utilisateur. ButtonView .

Exemple :

  • (E) est EditView le parent de toutes les vues
  • (M) est MenuView une sous-vue de EditView
  • (B) est ButtonView une sous-vue de MenuView

Diagramme :

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Comme (B) est en dehors du cadre de (M), un toucher dans la région de (B) ne sera jamais envoyé à (M) - en fait, (M) n'analyse jamais le toucher dans ce cas, et le toucher est envoyé à l'objet suivant dans la hiérarchie.

Objectif : J'en déduis que le fait d'outrepasser hitTest:withEvent: peut résoudre ce problème, mais je ne comprends pas exactement comment. Dans mon cas, faut-il hitTest:withEvent: être remplacée dans EditView (ma vue supérieure "maître") ? Ou doit-elle être surchargée dans MenuView la vue supérieure directe du bouton qui ne reçoit pas de touches ? Ou est-ce que je ne vois pas les choses de la bonne façon ?

Si cela nécessite une longue explication, une bonne ressource en ligne serait utile - à l'exception des documents UIView d'Apple, qui n'ont pas été clairs pour moi.

Gracias.

152voto

Noam Points 521

J'ai modifié le code de la réponse acceptée pour qu'il soit plus générique - il gère les cas où la vue ne découpe pas les sous-vues dans ses limites, peut être cachée, et plus important encore : si les sous-vues sont des hiérarchies de vues complexes, la sous-vue correcte sera retournée.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.clipsToBounds) {
        return nil;
    }

    if (self.hidden) {
        return nil;
    }

    if (self.alpha == 0) {
        return nil;
    }

    for (UIView *subview in self.subviews.reverseObjectEnumerator) {
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        UIView *result = [subview hitTest:subPoint withEvent:event];

        if (result) {
            return result;
        }
    }

    return nil;
}

SWIFT 3

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

    if clipsToBounds || isHidden || alpha == 0 {
        return nil
    }

    for subview in subviews.reversed() {
        let subPoint = subview.convert(point, from: self)
        if let result = subview.hitTest(subPoint, with: event) {
            return result
        }
    }

    return nil
}

J'espère que cela aidera tous ceux qui tentent d'utiliser cette solution pour des cas d'utilisation plus complexes.

0 votes

Cela semble bon et merci de l'avoir fait. Cependant, j'ai une question : pourquoi faites-vous return [super hitTest:point withEvent:event] ; ? Ne devriez-vous pas simplement retourner nil s'il n'y a pas de sous-vue qui déclenche le toucher ? Apple dit que hitTest renvoie nil si aucune sous-vue ne contient la touche.

0 votes

Hm... Oui, ça sonne juste. De plus, pour un comportement correct, les objets doivent être itérés dans l'ordre inverse (car le dernier est le plus haut visuellement). Code modifié pour correspondre.

0 votes

Parfait, cela m'a aidé à créer un menu latéral personnalisé pour mon projet où j'ai introduit des boutons UIB personnalisés dans le menu.

33voto

toblerpwn Points 1517

Ok, j'ai fait quelques recherches et tests, voici comment faire hitTest:withEvent fonctionne - au moins à un haut niveau. Imaginez ce scénario :

  • (E) est EditView le parent de toutes les vues
  • (M) est MenuView une sous-vue de EditView
  • (B) est ButtonView une sous-vue de MenuView

Diagramme :

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Comme (B) est en dehors du cadre de (M), un toucher dans la région de (B) ne sera jamais envoyé à (M) - en fait, (M) n'analyse jamais le toucher dans ce cas, et le toucher est envoyé à l'objet suivant dans la hiérarchie.

Cependant, si vous mettez en œuvre hitTest:withEvent: dans (M), les tapotements effectués n'importe où dans l'application seront envoyés à (M) (ou du moins il en a connaissance). Vous pouvez écrire du code pour gérer le toucher dans ce cas et renvoyer l'objet qui doit recevoir le toucher.

Plus précisément, l'objectif de hitTest:withEvent: est de renvoyer l'objet qui doit recevoir le coup. Ainsi, dans (M) vous pourriez écrire du code comme ceci :

// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
    for (UIView *subview in self.subviews) {
        if (CGRectContainsPoint(subview.frame, point)) {
            return subview;
        }
    }

    // use this to pass the 'touch' onward in case no subviews trigger the touch
    return [super hitTest:point withEvent:event];
}

Je suis encore très novice dans cette méthode et ce problème, donc s'il y a des façons plus efficaces ou correctes d'écrire le code, veuillez commenter.

J'espère que cela aidera toute personne qui se posera cette question plus tard :)

0 votes

Merci, cela a fonctionné pour moi, bien que j'aie dû faire un peu plus de logique pour déterminer quelles sont les sous-vues qui doivent effectivement recevoir des hits. J'ai supprimé une rupture inutile de votre exemple.

0 votes

Lorsque le cadre de la vue secondaire était supérieur au cadre de la vue parent, l'événement de clic ne répondait pas ! Votre solution a permis de résoudre ce problème. Merci beaucoup :-)

0 votes

@toblerpwn (drôle de surnom :) ) vous devriez éditer cette excellente réponse plus ancienne pour la rendre très claire (UTILISEZ DES MAJUSCULES GRAS ET LARGES) dans quelle classe vous devez ajouter ceci. A la vôtre !

33voto

duan Points 4267

Dans Swift 5

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
    for member in subviews.reversed() {
        let subPoint = member.convert(point, from: self)
        guard let result = member.hitTest(subPoint, with: event) else { continue }
        return result
    }
    return nil
}

2 votes

Cela ne fonctionnera que si vous appliquez la dérogation à l'interface de l'utilisateur. direct Vue d'ensemble de la vue "mauvais élève".

2voto

michael_mackenzie Points 592

Ce que je ferais, c'est que la ButtonView et la MenuView existent au même niveau dans la hiérarchie des vues en les plaçant toutes deux dans un conteneur dont le cadre s'adapte complètement aux deux. De cette façon, la région interactive de l'élément coupé ne sera pas ignorée à cause des limites de la vue supérieure.

0 votes

J'ai également pensé à cette solution de contournement - cela signifie que je vais devoir dupliquer une partie de la logique de placement (ou remanier un code sérieux !), mais cela pourrait bien être mon meilleur choix au final

1voto

paras gupta Points 41

Si vous avez beaucoup d'autres vues secondaires à l'intérieur de votre vue parent, alors la plupart des autres vues interactives ne fonctionneront probablement pas si vous utilisez les solutions ci-dessus. Dans ce cas, vous pouvez utiliser quelque chose comme ceci (dans Swift 3.2) :

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X