Permitir interacción con UIView bajo otra UIView

¿Existe una forma simple de permitir la interacción con un botón en una UIView que se encuentra debajo de otra UIView, donde no hay objetos reales desde la parte superior de UIView en la parte superior del botón?

Por ejemplo, en este momento tengo una UIView (A) con un objeto en la parte superior y un objeto en la parte inferior de la pantalla y nada en el medio. Esto se encuentra encima de otra UIView que tiene botones en el medio (B). Sin embargo, parece que no puedo interactuar con los botones en el medio de B.

Puedo ver los botones en B – Establecí el fondo de A para borrar Color – pero los botones en B no parecen recibir toques a pesar del hecho de que no hay objetos de A en realidad encima de esos botones.

EDITAR – Todavía quiero poder interactuar con los objetos en la parte superior de UIView

Seguramente hay una manera simple de hacer esto?

Debe crear una subclase UIView para su vista superior y anular el siguiente método:

 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { // UIView will be "transparent" for touch events if we return NO return (point.y < MIDDLE_Y1 || point.y > MIDDLE_Y2); } 

También puede consultar el método hitTest: event:

Si bien muchas de las respuestas aquí funcionarán, estoy un poco sorprendido de ver que la respuesta más conveniente, genérica e infalible no se ha dado aquí. @Ash fue el que estuvo más cerca, excepto que está sucediendo algo extraño al devolver la super visión … no hagas eso.

Esta respuesta está tomada de una respuesta que di a una pregunta similar, aquí .

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

[super hitTest:point withEvent:event] devolverá la vista más profunda en la jerarquía de esa vista que se tocó. Si hitView == self (es decir, si no hay subvista debajo del punto de contacto), devuelve nil , especificando que esta vista no debería recibir el toque. La forma en que funciona la cadena de respuesta significa que la jerarquía de vista por encima de este punto continuará atravesada hasta que se encuentre una vista que responderá al toque. No devuelva la supervista, ya que no depende de esta vista si su supervista debe aceptar toques o no.

Esta solución es:

  • conveniente , porque no requiere referencias a otras vistas / subvistas / objetos;
  • genérico , porque se aplica a cualquier vista que actúa puramente como un contenedor para subvistas tangibles, y la configuración de las subvistas no afecta la forma en que funciona (como lo hace si reemplaza pointInside:withEvent: para devolver un área pointInside:withEvent: en particular).
  • infalible , no hay mucho código … y el concepto no es difícil de entender.

Utilizo esto con la suficiente frecuencia que lo he abstraído en una subclase para guardar subclases de vista sin sentido para una anulación. Como bonificación, agregue una propiedad para que sea configurable:

 @interface ISView : UIView @property(nonatomic, assign) BOOL onlyRespondToTouchesInSubviews; @end @implementation ISView - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitView = [super hitTest:point withEvent:event]; if (hitView == self && onlyRespondToTouchesInSubviews) return nil; return hitView; } @end 

Entonces enloquece y usa esta vista donde sea que uses un UIView simple. Configurarlo es tan simple como configurar solo onlyRespondToTouchesInSubviews a YES .

Hay varias formas en que puede manejar esto. Mi favorito es anular hitTest: withEvent: en una vista que es una supervista común (tal vez indirectamente) a las vistas en conflicto (suena como si llamaras A y B). Por ejemplo, algo como esto (aquí A y B son punteros UIView, donde B es el “oculto”, que normalmente se ignora):

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGPoint pointInB = [B convertPoint:point fromView:self]; if ([B pointInside:pointInB withEvent:event]) return B; return [super hitTest:point withEvent:event]; } 

También puede modificar el pointInside:withEvent: como se sugiere gyim. Esto le permite lograr esencialmente el mismo resultado al “abrir un agujero” efectivamente en A, al menos para toques.

Otro enfoque es el reenvío de eventos, lo que significa touchesBegan:withEvent: y métodos similares (como touchesMoved:withEvent: etc) para enviar algunos toques a un objeto diferente de donde van primero. Por ejemplo, en A, podrías escribir algo como esto:

 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if ([self shouldForwardTouches:touches]) { [B touchesBegan:touches withEvent:event]; } else { // Do whatever A does with touches. } } 

Sin embargo, esto no siempre funcionará de la manera esperada. Lo principal es que los controles incorporados como UIButton siempre ignorarán los toques reenviados. Debido a esto, el primer enfoque es más confiable.

Hay una buena publicación en el blog que explica todo esto con más detalle, junto con un pequeño proyecto xcode en funcionamiento para hacer una demostración de las ideas, disponible aquí:

http://bynomial.com/blog/?p=74

upperView.userInteractionEnabled = NO; establecer upperView.userInteractionEnabled = NO; , de lo contrario, la vista superior interceptará los toques.

La versión de Interface Builder de esto es una checkbox en la parte inferior del panel Atributos de vista llamada “Interacción del usuario habilitada”. Desálzala y deberías ir bien.

Implementación personalizada de pointInside: withEvent: de hecho parecía el camino a seguir, pero lidiar con coordenadas rígidas me parecía extraño. Así que terminé comprobando si el CGPoint estaba dentro del botón CGRect usando la función CGRectContainsPoint ():

 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return (CGRectContainsPoint(disclosureButton.frame, point)); } 

Últimamente escribí una clase que me ayudará con eso. Utilizándolo como una clase personalizada para un UIButton o UIView pasará eventos táctiles que se ejecutaron en un píxel transparente.

Esta solución es algo mejor que la respuesta aceptada porque aún puede hacer clic en un UIButton que se encuentra bajo una UIView semitransparente, mientras que la parte no transparente de UIView responderá a los eventos táctiles.

GIF

Como puede ver en el GIF, el botón Jirafa es un rectángulo simple, pero los eventos táctiles en áreas transparentes se transmiten al UIButton amarillo que se UIButton debajo.

Enlace a la clase

Supongo que llego un poco tarde a esta fiesta, pero añadiré esta posible solución:

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

Si usa este código para anular la función de prueba de hit estándar de UIView, ignorará SOLAMENTE la vista en sí. Todas las subvistas de esa vista devolverán sus visitas normalmente, y todas las visitas que se hubieran enviado a la vista se pasarán a su super visión.

-Ceniza

Basta con pasar la Respuesta Aceptada y poner esto aquí para mi referencia. La respuesta aceptada funciona perfectamente. Puede extenderlo así para permitir que las subvistas de su vista reciban el toque, O pasarlo a cualquier punto de vista detrás de nosotros:

 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { // If one of our subviews wants it, return YES for (UIView *subview in self.subviews) { CGPoint pointInSubview = [subview convertPoint:point fromView:self]; if ([subview pointInside:pointInSubview withEvent:event]) { return YES; } } // otherwise return NO, as if userInteractionEnabled were NO return NO; } 

Nota: Ni siquiera tiene que hacer recursividad en el árbol de subvista, porque cada pointInside:withEvent: se encargará de eso por usted.

Establecer la propiedad de interacción de usuario deshabilitada puede ser útil. P.ej:

 UIView * topView = [[TOPView alloc] initWithFrame:[self bounds]]; [self addSubview:topView]; [topView setUserInteractionEnabled:NO]; 

(Nota: en el código de arriba, ‘self’ se refiere a una vista)

De esta forma, solo puede mostrar en la vista superior, pero no obtendrá entradas del usuario. Todos los toques de usuario lo harán a través de esta vista y la vista inferior responderá por ellos. Usaría este topView para mostrar imágenes transparentes o animarlas.

Este enfoque es bastante limpio y permite que las subvistas transparentes no reaccionen a los toques también. Solo subclase UIView y agregue el siguiente método para su implementación:

 @implementation PassThroughUIView - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { for (UIView *v in self.subviews) { CGPoint localPoint = [v convertPoint:point fromView:self]; if (v.alpha > 0.01 && ![v isHidden] && v.userInteractionEnabled && [v pointInside:localPoint withEvent:event]) return YES; } return NO; } @end 

Mi solución aquí:

 -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGPoint pointInView = [self.toolkitController.toolbar convertPoint:point fromView:self]; if ([self.toolkitController.toolbar pointInside:pointInView withEvent:event]) { self.userInteractionEnabled = YES; } else { self.userInteractionEnabled = NO; } return [super hitTest:point withEvent:event]; } 

Espero que esto ayude

Hay algo que puedes hacer para interceptar el toque en ambas vistas.

Vista superior:

 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // Do code in the top view [bottomView touchesBegan:touches withEvent:event]; // And pass them on to bottomView // You have to implement the code for touchesBegan, touchesEnded, touchesCancelled in top/bottom view. } 

Pero esa es la idea.

Aquí hay una versión de Swift:

 override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { return !CGRectContainsPoint(buttonView.frame, point) } 

Swift 3

 override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { for subview in subviews { if subview.frame.contains(point) { return true } } return false } 

Nunca he construido una interfaz de usuario completa utilizando el kit de herramientas de la interfaz de usuario, por lo que no tengo mucha experiencia con él. Esto es lo que creo que debería funcionar.

Cada UIView, y esta la UIWindow, tiene una propiedad de subviews , que es un NSArray que contiene todas las subvistas.

La primera subvista que agregue a una vista recibirá el índice 0, y el siguiente índice 1, y así sucesivamente. También puede reemplazar addSubview: con insertSubview: atIndex: o insertSubview:aboveSubview: y los métodos que pueden determinar la posición de su subvista en la jerarquía.

Por lo tanto, verifique su código para ver qué vista agrega primero a su UIWindow. Eso será 0, el otro será 1.
Ahora, desde una de sus subvistas, para llegar a otra, haría lo siguiente:

 UIView * theOtherView = [[[self superview] subviews] objectAtIndex: 0]; // or using the properties syntax UIView * theOtherView = [self.superview.subviews objectAtIndex:0]; 

¡Avíseme si eso funciona para su caso!


(debajo de este marcador está mi respuesta anterior):

Si las vistas necesitan comunicarse entre sí, deben hacerlo a través de un controlador (es decir, utilizando el popular modelo MVC ).

Cuando creas una nueva vista, puedes asegurarte de que se registre con un controlador.

Entonces la técnica es asegurarse de que sus vistas se registren con un controlador (que puede almacenarlas por nombre o lo que prefiera en un Diccionario o Matriz). O puede hacer que el controlador le envíe un mensaje, o puede obtener una referencia a la vista y comunicarse con ella directamente.

Si su vista no tiene un enlace de vuelta al controlador (que puede ser el caso), entonces puede hacer uso de singletons y / o métodos de clase para obtener una referencia a su controlador.

Creo que la forma correcta es usar la cadena de vista integrada en la jerarquía de vistas. Para las subvistas que se insertan en la vista principal, no use el UIView genérico, sino la subclase UIView (o una de sus variantes como UIImageView) para hacer MYView: UIView (o el supertipo que desee, como UIImageView). En la implementación de YourView, implemente el método touchesBegan. Este método se invocará cuando se toque esa vista. Todo lo que necesita tener en esa implementación es un método de instancia:

 - (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event ; { // cannot handle this event. pass off to super [self.superview touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event]; } 

esto toca Bean es una API respondedora, por lo que no necesita declararla en su interfaz pública o privada; es una de esas aplicaciones mágicas que solo debes conocer. Esta auto-supervisión generará la solicitud eventualmente a viewController. En viewController, implemente estos toques Bean para manejar el toque.

Tenga en cuenta que la ubicación de toques (CGPoint) se ajusta automáticamente en relación con la vista que abarca, ya que se rebota en la cadena de jerarquía de vista.

Solo quiero publicar esto, porque tuve un problema similar, pasé una gran cantidad de tiempo tratando de implementar las respuestas aquí sin suerte. Lo que terminé haciendo:

  for(UIGestureRecognizer *recognizer in topView.gestureRecognizers) { recognizer.delegate=self; [bottomView addGestureRecognizer:recognizer]; } topView.abView.userInteractionEnabled=NO; 

e implementando UIGestureRecognizerDelegate :

 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; } 

La vista inferior era un controlador de navegación con número de segues y tenía una especie de puerta encima que podía cerrarse con un gesto panorámico. Todo estaba incrustado en otro VC. Trabajado como un encanto. Espero que esto ayude.

Implementación de Swift 4 para la solución basada en HitTest

 let hitView = super.hitTest(point, with: event) if hitView == self { return nil } return hitView