Manejo de eventos para iOS: cómo hitTest: withEvent: y pointInside: withEvent: ¿están relacionados?

Si bien la mayoría de los documentos de Apple están muy bien escritos, creo que la ‘ Guía de manejo de eventos para iOS ‘ es una excepción. Es difícil para mí entender claramente lo que se ha descrito allí.

El documento dice,

En la prueba de aciertos, una ventana llama a hitTest:withEvent: en la vista superior de la jerarquía de vistas; este método procede llamando recursivamente a pointInside:withEvent: en cada vista en la jerarquía de vista que devuelve SÍ, avanzando hacia abajo en la jerarquía hasta que encuentra la subvista dentro de cuyos límites se realizó el toque. Esa vista se convierte en la vista de prueba de golpe.

Entonces, ¿solo es así hitTest:withEvent: de la vista más alta es pointInside:withEvent: por el sistema, que llama a pointInside:withEvent: de todas las subvistas, y si el retorno de una subvista específica es YES, entonces llama a pointInside:withEvent: de las subclases de esa subvista?

Parece una pregunta bastante básica. Pero estoy de acuerdo con usted, el documento no es tan claro como otros documentos, así que esta es mi respuesta.

La implementación de hitTest:withEvent: en UIResponder hace lo siguiente:

  • Llama pointInside:withEvent: of self
  • Si el resultado es NO, hitTest:withEvent: devuelve nil . El fin de la historia.
  • Si el resultado es SÍ, envía hitTest:withEvent: messages a sus subvistas. comienza desde la subvista de nivel superior y continúa hasta otras vistas hasta que una subvista devuelve un objeto no nil o todas las subvistas reciben el mensaje.
  • Si una subvista devuelve un objeto no nil la primera vez, el primer hitTest:withEvent: devuelve ese objeto. El fin de la historia.
  • Si ninguna subvista devuelve un objeto no nil , la primera hitTest:withEvent: devuelve self

Este proceso se repite recursivamente, por lo que normalmente la vista de hoja de la jerarquía de vista se devuelve finalmente.

Sin embargo, puede anular hitTest:withEvent para hacer algo diferente. En muchos casos, anulando pointInside:withEvent: es más simple y aún proporciona suficientes opciones para modificar el manejo de eventos en su aplicación.

Creo que estás confundiendo las subclases con la jerarquía de vistas. Lo que dice el documento es el siguiente. Supongamos que tiene esta jerarquía de vistas. Por jerarquía, no estoy hablando de jerarquía de clases, sino de vistas dentro de la jerarquía de vistas, de la siguiente manera:

 +----------------------------+ |A | |+--------+ +------------+ | ||B | |C | | || | |+----------+| | |+--------+ ||D || | | |+----------+| | | +------------+ | +----------------------------+ 

Digamos que metiste el dedo dentro D Esto es lo que sucederá:

  1. hitTest:withEvent: se hitTest:withEvent: en A , la vista superior de la jerarquía de vistas.
  2. pointInside:withEvent: se llama recursivamente en cada vista.
    1. pointInside:withEvent: se pointInside:withEvent: a A y devuelve YES
    2. pointInside:withEvent: se pointInside:withEvent: en B y devuelve NO
    3. pointInside:withEvent: se pointInside:withEvent: a C y devuelve YES
    4. pointInside:withEvent: se pointInside:withEvent: en D y devuelve YES
  3. En las vistas que devolvieron YES , mirarán hacia abajo en la jerarquía para ver la subvista donde se realizó el toque. En este caso, de A , C y D , será D
  4. D será la vista de prueba

Encuentro este Hit-Testing en iOS para ser muy útil

enter image description here

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (!self.isUserInteractionEnabled || self.isHidden || self.alpha < = 0.01) { return nil; } if ([self pointInside:point withEvent:event]) { for (UIView *subview in [self.subviews reverseObjectEnumerator]) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { return hitTestView; } } return self; } return nil; } 

Editar Swift 4:

 override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.point(inside: point, with: event) { return super.hitTest(point, with: event) } guard isUserInteractionEnabled, !isHidden, alpha > 0 else { return nil } for subview in subviews.reversed() { let convertedPoint = subview.convert(point, from: self) if let hitView = subview.hitTest(convertedPoint, with: event) { return hitView } } return nil } 

Gracias por las respuestas, me ayudaron a resolver la situación con vistas “superpuestas”.

 +----------------------------+ |A +--------+ | | |B +------------------+ | | | |CX | | | | +------------------+ | | | | | | +--------+ | | | +----------------------------+ 

Asum X – toque del usuario. pointInside:withEvent: on B devuelve NO , así que hitTest:withEvent: devuelve A Escribí una categoría en UIView para manejar el problema cuando necesitas recibir el toque en la vista más visible .

 - (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event { // 1 if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0) return nil; // 2 UIView *hitView = self; if (![self pointInside:point withEvent:event]) { if (self.clipsToBounds) return nil; else hitView = nil; } // 3 for (UIView *subview in [self.subviewsreverseObjectEnumerator]) { CGPoint insideSubview = [self convertPoint:point toView:subview]; UIView *sview = [subview overlapHitTest:insideSubview withEvent:event]; if (sview) return sview; } // 4 return hitView; } 
  1. No debemos enviar eventos táctiles para vistas ocultas o transparentes, o vistas con userInteractionEnabled establecido en NO ;
  2. Si el tacto está dentro de self , el self se considerará como resultado potencial.
  3. Compruebe recursivamente todas las subvistas para golpear. Si hay alguno, devuélvalo.
  4. De lo contrario, devuelve self o nil dependiendo del resultado del paso 2.

Tenga en cuenta que [self.subviewsreverseObjectEnumerator] necesario para seguir la jerarquía de vista desde arriba hacia abajo. Y compruebe si hay clipsToBounds en los clipsToBounds para asegurarse de no probar las subvistas enmascaradas.

Uso:

  1. Importar categoría en su vista subclase.
  2. Reemplazar hitTest:withEvent: con esto
 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { return [self overlapHitTest:point withEvent:event]; } 

La Guía oficial de Apple también proporciona algunas buenas ilustraciones.

Espero que esto ayude a alguien.

Se muestra como este fragmento!

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01) { return nil; } if (![self pointInside:point withEvent:event]) { return nil; } __block UIView *hitView = self; [self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { CGPoint thePoint = [self convertPoint:point toView:obj]; UIView *theSubHitView = [obj hitTest:thePoint withEvent:event]; if (theSubHitView != nil) { hitView = theSubHitView; *stop = YES; } }]; return hitView; } 

El fragmento de @lion funciona como un hechizo. Lo porté para acelerar 2.1 y lo usé como una extensión para UIView. Lo estoy publicando aquí en caso de que alguien lo necesite.

 extension UIView { func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? { // 1 if !self.userInteractionEnabled || self.hidden || self.alpha == 0 { return nil } //2 var hitView: UIView? = self if !self.pointInside(point, withEvent: event) { if self.clipsToBounds { return nil } else { hitView = nil } } //3 for subview in self.subviews.reverse() { let insideSubview = self.convertPoint(point, toView: subview) if let sview = subview.overlapHitTest(insideSubview, withEvent: event) { return sview } } return hitView } } 

Para usarlo, solo anule hitTest: point: withEvent en su uiview de la siguiente manera:

 override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? { let uiview = super.hitTest(point, withEvent: event) print("hittest",uiview) return overlapHitTest(point, withEvent: event) }