Capturer des touches sur une sous-vue en dehors du cadre de sa supervision à l'aide de hitTest: withEvent:

94

Mon problème: j'ai un superview EditViewqui occupe essentiellement tout le cadre de l'application, et une sous-vue MenuViewqui ne prend que les ~ 20% inférieurs, puis MenuViewcontient sa propre sous-vue ButtonViewqui réside en fait en dehors des limites de MenuView's (quelque chose comme ceci :) ButtonView.frame.origin.y = -100.

(Remarque: EditViewa d'autres sous-vues qui ne font pas partie de MenuViewla hiérarchie des vues de, mais peuvent affecter la réponse.)

Vous connaissez probablement déjà le problème: quand ButtonViewest dans les limites de MenuView(ou, plus précisément, lorsque mes touches sont dans MenuViewles limites de), ButtonViewrépond aux événements tactiles. Lorsque mes touches sont en dehors des limites de MenuView's (mais toujours dans ButtonViewles limites de'), aucun événement tactile n'est reçu par ButtonView.

Exemple:

  • (E) est EditViewle parent de toutes les vues
  • (M) est MenuViewune sous-vue de EditView
  • (B) est ButtonViewune sous-vue de MenuView

Diagramme:

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

Parce que (B) est en dehors du cadre de (M), un tap dans la région (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: je hitTest:withEvent:crois comprendre que le dépassement peut résoudre ce problème, mais je ne comprends pas exactement comment. Dans mon cas, doit-il hitTest:withEvent:être remplacé dans EditView(mon superview «maître»)? Ou devrait-il être remplacé dans MenuViewla vue directe du bouton qui ne reçoit pas de touches? Ou est-ce que je pense mal à cela?

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

Merci!

toblerpwn
la source

Réponses:

145

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 coupe les sous-vues à ses limites, peut être masquée et, plus important encore: si les sous-vues sont des hiérarchies de vues complexes, la sous-vue correcte sera renvoyé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 quiconque essaiera d'utiliser cette solution pour des cas d'utilisation plus complexes.

Noam
la source
Cela semble bon et merci de l'avoir fait. Cependant, j'avais une question: pourquoi faites-vous return [super hitTest: point withEvent: event]; ? Ne renverriez-vous pas simplement nul s'il n'y a pas de sous-vue qui déclenche le toucher? Apple dit que hitTest renvoie nul si aucune sous-vue ne contient le toucher.
Ser Pounce
Hm ... Ouais, ça semble juste. En outre, 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.
Noam
3
Je viens d'utiliser votre solution pour qu'un UIButton capture le toucher, et c'est à l'intérieur d'un UIView qui se trouve à l'intérieur d'un UICollectionViewCell à l'intérieur (évidemment) d'un UICollectionView. J'ai dû sous-classer UICollectionView et UICollectionViewCell pour remplacer hitTest: withEvent: sur ces trois classes. Et ça marche comme du charme !! Merci !!
Daniel García
3
Selon l'utilisation souhaitée, il doit renvoyer [super hitTest: point withEvent: event] ou nil. Revenir à soi lui ferait tout recevoir.
Noam
1
Voici un document technique de questions-réponses d'Apple sur cette même technique: developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang
33

Ok, j'ai fait des recherches et des tests, voici comment hitTest:withEventfonctionne - au moins à un niveau élevé. 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                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Parce que (B) est en dehors du cadre de (M), un tap dans la région (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 implémentez hitTest:withEvent:dans (M), les taps n'importe où dans l'application seront envoyés à (M) (ou du moins il les connaît). Vous pouvez écrire du code pour gérer le toucher dans ce cas et renvoyer l'objet qui devrait recevoir le toucher.

Plus précisément: le but de hitTest:withEvent:est de renvoyer l'objet qui devrait recevoir le hit. Donc, dans (M), vous pouvez écrire du code comme celui-ci:

// 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 nouveau dans cette méthode et ce problème, donc s'il existe des moyens plus efficaces ou plus corrects d'écrire le code, veuillez commenter.

J'espère que cela aidera quiconque abordera cette question plus tard. :)

toblerpwn
la source
Merci, cela a fonctionné pour moi, même si j'ai dû faire plus de logique pour déterminer laquelle des sous-vues qui devrait réellement recevoir des hits. J'ai supprimé une pause inutile de votre exemple btw.
Daniel Saidi
Lorsque le cadre de sous-vue était au-delà du cadre de la vue parent, l'événement de clic ne répondait pas! Votre solution a résolu ce problème. Merci beaucoup :-)
parJeevan
@toblerpwn (surnom drôle :)), vous devriez éditer cette excellente réponse plus ancienne pour indiquer très clairement (UTILISER DE GRANDES MAJUSCULES GRAS) dans quelle classe vous devez ajouter ceci. À votre santé!
Fattie
26

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
}
duan
la source
Cela ne fonctionnera que si vous appliquez le remplacement sur la supervision directe de la vue "mal comporté"
Hudi Ilfeld
2

Ce que je ferais, c'est que ButtonView et MenuView existent au même niveau dans la hiérarchie des vues en les plaçant tous les 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 découpé ne sera pas ignorée en raison des limites de la vue supervisée.

Mamackenzie
la source
J'ai aussi pensé à cette solution de contournement - cela signifie que je
devrai
1

Si vous avez de nombreuses autres sous-vues dans votre vue parent, alors la plupart des autres vues interactives ne fonctionneraient 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)
    }
}
Paras gupta
la source
0

Si quelqu'un en a besoin, voici l'alternative rapide

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
        for subview in self.subviews.reverse() {
            let subPoint = subview.convertPoint(point, fromView:self);

            if let result = subview.hitTest(subPoint, withEvent:event) {
                return result;
            }
        }
    }

    return nil
}
Fiston
la source
0

Placez sous les lignes de code dans votre hiérarchie de vues:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* hitView = [super hitTest:point withEvent:event];
    if (hitView != nil)
    {
        [self.superview bringSubviewToFront:self];
    }
    return hitView;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

Pour plus de précision, il a été expliqué dans mon blog: "goaheadwithiphonetech" concernant "Légende personnalisée: le bouton n'est pas cliquable".

J'espère que cela vous aide ... !!!

Sandip Patel - SM
la source
Votre blog a été supprimé, alors où pouvons-nous trouver l'explication, s'il vous plaît?
ishahak