Capture de touches dans une sous-vue en dehors du cadre de sa vue d'set à l'aide de hitTest: withEvent:

Mon problème: J'ai une EditView qui reprend essentiellement l'set du cadre de l'application, et une sous-vue MenuView qui occupe seulement le bas ~ 20%, puis MenuView contient sa propre sous-vue ButtonView qui réside réellement en dehors des MenuView de MenuView (quelque chose comme ceci: ButtonView.frame.origin.y = -100 ).

(remarque: EditView a d'autres sous-vues qui ne font pas partie de la MenuView de MenuView de MenuView , mais peuvent 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 dans les limites de ButtonView ), ButtonView répond aux events tactiles. Lorsque mes touches sont en dehors des MenuView de MenuView (mais toujours dans les limites de ButtonView ), aucun événement tactile n'est reçu par 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 || || || |+----------------------------+| +------------------------------+ 

Parce que (B) est en dehors de la trame (M), un tap dans la région (B) ne sera jamais envoyé à (M) – en fait, (M) n'parsing jamais le toucher dans ce cas, et le toucher est envoyé à l'object suivant dans la hiérarchie.

But: Je hitTest:withEvent: que hitTest:withEvent: peut résoudre ce problème, mais je ne comprends pas exactement comment. Dans mon cas, est-ce que hitTest:withEvent: être EditView dans EditView (mon EditView 'master')? Ou devrait-il être MenuView dans MenuView , l' MenuView direct du button qui ne reçoit pas de touches? Ou est-ce que j'y pense incorrectement?

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 éclairci.

Merci!

J'ai modifié le code de la réponse acceptée pour être plus générique – il gère les cas où la vue découpe les sous-vues à ses limites, peut être caché, et plus important: si les sous-vues sont des hiérarchies complexes, la sous-vue correcte sera returnné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 essaient d'utiliser cette solution pour des cas d'utilisation plus complexes.

Ok, j'ai fait quelques searchs et tests, voici comment fonctionne hitTest:withEvent – au less à un haut niveau. Image 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 de la trame (M), un tap dans la région (B) ne sera jamais envoyé à (M) – en fait, (M) n'parsing jamais le toucher dans ce cas, et le toucher est envoyé à l'object 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 less il en est informé). Vous pouvez écrire du code pour gérer le toucher dans ce cas et returnner l'object qui devrait recevoir le toucher.

Plus spécifiquement: le but de hitTest:withEvent: est de renvoyer l'object qui devrait recevoir le hit. Donc, 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 sortinggger the touch return [super hitTest:point withEvent:event]; } 

Je suis encore très nouveau à cette méthode et ce problème, donc s'il y a des façons plus efficaces ou correctes d'écrire le code, s'il vous plaît commenter.

J'espère que cela aidera quelqu'un d'autre qui répondra à cette question plus tard. 🙂

Dans Swift3

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

Ce que je ferais, c'est que ButtonView et MenuView existent au même niveau dans la hiérarchie de la vue en les plaçant tous les deux dans un conteneur dont le cadre correspond parfaitement aux deux. De cette façon, la région interactive de l'élément découpé ne sera pas ignorée en raison de ses limites.

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 } 

Si vous avez beaucoup d'autres sous-vues dans votre vue parent alors probablement la plupart des autres vues interactives ne fonctionneraient pas si vous utilisez les solutions ci-dessus, dans ce cas vous pouvez utiliser quelque chose comme ça (In 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) } } 

Placez ci-dessous des lignes de code dans votre hiérarchie de vue:

 - (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écisions, il a été expliqué dans mon blog: "goaheadwithiphonetech" à propos de "Custom callout: Button is not clickable issue".

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