Permitir la interacción con una UIView bajo otra UIView


115

¿Existe una forma sencilla de permitir la interacción con un botón en una UIView que se encuentra debajo de otra UIView, donde no hay objetos reales de la UIView superior 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 - he configurado el fondo de A en clearColor - pero los botones en B no parecen recibir toques a pesar del hecho de que no hay objetos de A realmente encima de esos botones.

EDITAR : todavía quiero poder interactuar con los objetos en la UIView superior

¿Seguro que hay una forma sencilla de hacer esto?


2
Prácticamente todo se explica aquí: developer.apple.com/iphone/library/documentation/iPhone/… Pero básicamente, anula hitTest: withEvent :, incluso proporcionan una muestra de código.
Nash

He escrito una clase pequeña solo para eso. (Se agregó un ejemplo en las respuestas). La solución es algo mejor que la respuesta aceptada porque aún puede hacer clic en un UIButtonque está debajo de un semitransparente, UIViewmientras que la parte no transparente del UIViewaún responderá a los eventos táctiles.
Segev

Respuestas:


97

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 mirar el método hitTest: event:.


19
¿Qué representa middle_y1 / y2 aquí?
Jason Renaldo

No estoy seguro de lo que returnhace esa declaración, pero return CGRectContainsPoint(eachSubview.frame, point)funciona para mí. Respuesta extremadamente útil de lo contrario
n00neimp0rtant

El negocio MIDDLE_Y1 / Y2 es solo un ejemplo. Esta función será "transparente" para eventos táctiles en el MIDDLE_Y1<=y<=MIDDLE_Y2área.
Gyim

41

Si bien muchas de las respuestas aquí funcionarán, estoy un poco sorprendido de ver que aquí no se ha dado la respuesta más conveniente, genérica e infalible. @Ash estuvo más cerca, excepto que hay algo extraño al devolver la supervista ... 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 una subvista debajo del punto de contacto), regrese nil, especificando que esta vista no debe recibir el toque. La forma en que funciona la cadena de respuesta significa que la jerarquía de vistas por encima de este punto se seguirá recorriendo hasta que se encuentre una vista que responda 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 simplemente como un contenedor de subvistas táctiles, y la configuración de las subvistas no afecta la forma en que funciona (como lo hace si anulapointInside:withEvent: para devolver un área táctil 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 beneficio adicional, 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 vuélvase loco y use esta vista donde quiera que pueda usar un plano UIView. Configuración es tan simple como la creación onlyRespondToTouchesInSubviewsde YES.


2
La única respuesta correcta. para ser claros, si desea ignorar los toques a una vista, pero no a los botones (digamos) que están contenidos en la vista , haga lo que explica Stuart. (Normalmente lo llamo una "vista de titular" porque puede "mantener" algunos botones sin
causar

Algunas de las otras soluciones que usan puntos tienen problemas si su vista es desplazable, como UITableView og UICollectionView y se ha desplazado hacia arriba o hacia abajo. Sin embargo, esta solución funciona independientemente del desplazamiento.
pajevic

31

Hay varias formas de 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 (parece que las llama 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:método como sugirió gyim. Esto le permite lograr esencialmente el mismo resultado "haciendo un agujero" en A, al menos para los toques.

Otro enfoque es el reenvío de eventos, lo que significa anular touchesBegan:withEvent:y métodos similares (como, touchesMoved:withEvent:etc.) para enviar algunos toques a un objeto diferente al que tenían por primera vez. Por ejemplo, en A, podría 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 integrados como UIButton siempre ignorarán los toques reenviados. Debido a esto, el primer enfoque es más confiable.

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

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


El problema con esta solución, que anula hitTest en la supervista común, es que no permite que la vista inferior funcione completamente correctamente cuando la vista inferior es una de un conjunto completo de vistas desplazables (MapView, etc.). Overriding pointInside en la vista superior, como sugiere gyim a continuación, funciona en todos los casos, por lo que puedo decir.
delany

@delany, eso no es cierto; puede tener vistas desplazables ubicadas debajo de otras vistas y dejar que ambas funcionen anulando hitTest. Aquí hay un código de muestra: bynomial.com/blogfiles/Temp32.zip
Tyler

Oye. No todas las vistas desplazables, solo algunas ... Probé su código postal desde arriba, cambié UIScrollView a (por ejemplo) MKMapView y no funciona. Los toques funcionan, es el desplazamiento lo que parece ser el problema.
delany

Ok, lo verifiqué y confirmé que hitTest no funciona de la manera que quisiera con MKMapView. Tenías razón en eso, @delany; aunque funciona correctamente con UIScrollView's. Me pregunto por qué falla MKMapView.
Tyler

@Tyler Como mencionaste, que "Lo principal es que los controles integrados como UIButton siempre ignorarán los toques reenviados", quiero saber cómo lo sabes y si es un documento oficial para explicar este comportamiento. Me enfrenté al problema de que cuando UIButton tiene una subvista UIView simple, no responderá a los eventos táctiles en los límites de la subvista. Y descubrí que el evento se reenvía al UIButton correctamente como el comportamiento predeterminado de UIView, pero no estoy seguro de que sea una función designada o simplemente un error. ¿Podría mostrarme la documentación sobre esto? Muchas gracias sinceramente.
Neal.Marlin

29

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

La versión de Interface Builder de esto es una casilla de verificación en la parte inferior del panel Ver atributos llamada "Interacción del usuario habilitada". Desmárquelo y estará listo para comenzar.


Lo siento, debería haber dicho. Todavía quiero poder interactuar con los objetos en el UIView superior.
delany

Pero la vista superior no puede recibir ningún toque, incluya el botón en la vista superior.
imcaptor

2
Esta solución funciona para mí. No necesitaba la vista superior para reaccionar a los toques en absoluto.
TJ

11

La implementación personalizada de pointInside: withEvent: de hecho parecía el camino a seguir, pero tratar con coordenadas codificadas de forma rígida me parecía extraño. Así que terminé verificando 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));
}

8

Últimamente escribí una clase que me ayudará con eso. Usarlo como una clase personalizada para un UIButtono UIViewpasará 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 UIButtonque está debajo de un semitransparente UIViewmientras que la parte no transparente del UIViewaún 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 pasan al amarillo UIButtondebajo.

Enlace a la clase


2
Dado que su código no es tan largo, debe incluir las partes relevantes en su respuesta, en caso de que su proyecto se mueva o elimine en algún momento en el futuro.
Gavin

Gracias, su solución funciona para mi caso en el que necesito que la parte no transparente de mi UIView responda a eventos táctiles, mientras que la parte transparente no. ¡Brillante!
Bruce

@Bruce ¡Me alegro de que te haya ayudado!
Segev

4

Supongo que llego un poco tarde a esta fiesta, pero agregaré 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 estándar hitTest de una UIView personalizada, ignorará SOLAMENTE la vista en sí. Cualquier subvista de esa vista devolverá sus visitas normalmente, y cualquier visita que hubiera ido a la vista misma se pasa a su supervista.

-Ceniza


1
Este es mi método preferido, excepto que no creo que debas regresar [self superview]. la documentación sobre este método indica "Devuelve el descendiente más lejano del receptor en la jerarquía de vista (incluido él mismo) que contiene un punto especificado" y "Devuelve nulo si el punto está completamente fuera de la jerarquía de vista del receptor". Creo que deberías volver nil. cuando devuelve nil, el control pasará a la supervista para que verifique si tiene algún acierto o no. así que básicamente hará lo mismo, excepto que devolver la supervista podría romper algo en el futuro.
jasongregori

Claro, eso probablemente sería prudente (tenga en cuenta la fecha en mi respuesta original, he hecho mucho más código desde entonces)
Ash

4

Simplemente riffing en la respuesta aceptada y poner esto aquí para mi referencia. La respuesta aceptada funciona perfectamente. Puede extenderlo de esta manera para permitir que las subvistas de su vista reciban el toque, O pasarlo a cualquier 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:método lo manejará por usted.


3

Establecer la propiedad userInteraction deshabilitada puede ayudar. P.ej:

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

(Nota: en el código anterior, 'self' se refiere a una vista)

De esta manera, solo puede mostrar en la vista superior, pero no obtendrá entradas de usuario. Todos esos toques de usuario pasarán por esta vista y la vista inferior responderá por ellos. Usaría esta vista superior para mostrar imágenes transparentes o animarlas.


3

Este enfoque es bastante limpio y permite que las subvistas transparentes no reaccionen también a los toques. Simplemente subclase UIViewy agregue el siguiente método a 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

2

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


2

Hay algo que puede 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.


Esto es ciertamente posible, pero es mucho trabajo (creo que tendría que enrollar sus propios objetos sensibles al tacto en la capa inferior (por ejemplo, botones)) y parece extraño que uno tenga que enrollar los suyos en este forma de obtener un comportamiento que parezca intuitivo.
delany

No estoy seguro de que debamos intentarlo.
Alexandre Cassagne

2

Aquí hay una versión Swift:

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

2

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
}

1

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

Cada UIView, y esta UIWindow, tiene una propiedad 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:oinsertSubview:aboveSubview: y métodos que pueden determinar la posición de su subvista en la jerarquía.

Así que verifique su código para ver qué vista agrega primero a su UIWindow. Ese 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ísame si eso funciona para tu 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 crea una nueva vista, puede asegurarse 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). 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 al controlador (que puede ser el caso), entonces puede hacer uso de métodos únicos y / o de clase para obtener una referencia a su controlador.


Gracias por su respuesta, pero no estoy seguro de haberlo entendido. Ambas vistas tienen un controlador; el problema es que, con una vista encima de la otra, la vista inferior no capta eventos (y los reenvía a su controlador), incluso si no hay ningún objeto que realmente 'bloquee' esos eventos en la vista superior.
delany

¿Tiene una UIWindow en la parte superior de nuestras UIViews? Si lo hace, los eventos deberían propagarse y no debería necesitar hacer ninguna "magia". Lea sobre [Ventana y vistas] [1] en el centro de desarrollo de Apple (y, por supuesto, agregue otro comentario si esto no le ayuda) [1]: developer.apple.com/iphone/library/documentation/ iPhone /…
nash

Sí, absolutamente, una UIWindow en la parte superior de la jerarquía.
delany

Actualicé mi respuesta para incluir el código para pasar por una jerarquía de vistas. ¡Déjame saber si necesitas cualquier otra ayuda!
Nash

Gracias por tu ayuda, nash, pero tengo bastante experiencia en la creación de estas interfaces y, por lo que sé, la actual está configurada correctamente. El problema parece ser el comportamiento predeterminado de las vistas completas cuando se colocan encima de otras.
delany

1

Creo que la forma correcta es utilizar la cadena de vistas integrada en la jerarquía de vistas. Para sus subvistas que se insertan en la vista principal, no use la UIView genérica, sino la subclase UIView (o una de sus variantes como UIImageView) para hacer MYView: UIView (o cualquier 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]; }

this touchesBegan es una API de respuesta, por lo que no necesita declararla en su interfaz pública o privada; es una de esas API mágicas que debes conocer. Este self.superview enviará la solicitud al viewController. En viewController, implemente este toque Comenzó a manejar el toque.

Tenga en cuenta que la ubicación de los toques (CGPoint) se ajusta automáticamente en relación con la vista abarcadora a medida que rebota en la cadena de jerarquía de vistas.


1

Solo quiero publicar esto, porque tuve un problema similar, pasé una cantidad considerable de tiempo tratando de implementar 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 un número de segues y tenía una especie de puerta encima que podía cerrarse con un gesto de panorámica. Todo estaba incrustado en otro VC. Trabajado como un encanto. Espero que esto ayude.


1

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

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

0

Derivado de la respuesta excelente y en su mayoría infalible de Stuart, y la implementación útil de Segev, aquí hay un paquete de Swift 4 que puede incluir en cualquier proyecto:

extension UIColor {
    static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {

        var pixel: [CUnsignedChar] = [0, 0, 0, 0]

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

        let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)

        context!.translateBy(x: -point.x, y: -point.y)

        view.layer.render(in: context!)

        let red: CGFloat   = CGFloat(pixel[0]) / 255.0
        let green: CGFloat = CGFloat(pixel[1]) / 255.0
        let blue: CGFloat  = CGFloat(pixel[2]) / 255.0
        let alpha: CGFloat = CGFloat(pixel[3]) / 255.0

        let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)

        return color
    }
}

Y luego con hitTest:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
    return super.hitTest(point, with: event)
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.