¿Cómo puedo hacer clic en un botón detrás de una UIView transparente?

177

Digamos que tenemos un controlador de vista con una vista secundaria. la subvista ocupa el centro de la pantalla con márgenes de 100 px en todos los lados. Luego agregamos un montón de pequeñas cosas para hacer clic dentro de esa subvista. Solo estamos usando la subvista para aprovechar el nuevo marco (x = 0, y = 0 dentro de la subvista es en realidad 100,100 en la vista principal).

Luego, imagine que tenemos algo detrás de la subvista, como un menú. Quiero que el usuario pueda seleccionar cualquiera de las "pequeñas cosas" en la subvista, pero si no hay nada allí, quiero que pasen toques (ya que el fondo está claro de todos modos) a los botones detrás de él.

¿Cómo puedo hacer esto? Parece que comienza a tocar, pero los botones no funcionan.

Sean Clark Hess
fuente
1
Pensé que las UIViews transparentes (alfa 0) no deberían responder a eventos táctiles.
Evadne Wu
1
He escrito una pequeña clase solo por eso. (Se agregó un ejemplo en las respuestas). La solución es algo mejor que la respuesta aceptada porque todavía puede hacer clic en una UIButtonque está debajo de un semitransparente, UIViewmientras que la parte no transparente de la UIViewrespuesta seguirá respondiendo a eventos táctiles.
Segev

Respuestas:

315

Cree una vista personalizada para su contenedor y anule el mensaje pointInside: para devolver false cuando el punto no está dentro de una vista secundaria elegible, como esta:

Rápido:

class PassThroughView: UIView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        for subview in subviews {
            if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
                return true
            }
        }
        return false
    }
}

C objetivo:

@interface PassthroughView : UIView
@end

@implementation PassthroughView
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    for (UIView *view in self.subviews) {
        if (!view.hidden && view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event])
            return YES;
    }
    return NO;
}
@end

El uso de esta vista como contenedor permitirá que cualquiera de sus hijos reciba toques, pero la vista en sí misma será transparente para los eventos.

John Stephen
fuente
44
Interesante. Necesito sumergirme más en la cadena de respuesta.
Sean Clark Hess
1
debe verificar si la vista también es visible, luego también debe verificar si las subvistas son visibles antes de probarlas.
The Lazy Coder
2
desafortunadamente este truco no funciona para un tabBarController, para el cual la vista no se puede cambiar. ¿Alguien tiene una idea para hacer que esta vista sea transparente para los eventos también?
cyrilchampier
1
También debe verificar el alfa de la vista. Muy a menudo esconderé una vista estableciendo el alfa en cero. Una vista con cero alfa debería actuar como una vista oculta.
James Andrews
2
Hice una versión rápida: tal vez pueda incluirla en la respuesta para visibilidad gist.github.com/eyeballz/17945454447c7ae766cb
eyeballz
107

Yo tambien uso

myView.userInteractionEnabled = NO;

No hay necesidad de subclase. Funciona bien.

akw
fuente
76
Esto también deshabilitará la interacción del usuario para cualquier subvista
pixelfreak
Como @pixelfreak mencionó, esta no es la solución ideal para todos los casos. Los casos en los que aún se desea la interacción en las subvistas de esa vista requieren que ese indicador permanezca habilitado.
Augusto Carmo
20

De Apple:

El reenvío de eventos es una técnica utilizada por algunas aplicaciones. Puede reenviar eventos táctiles invocando los métodos de manejo de eventos de otro objeto de respuesta. Aunque esta puede ser una técnica efectiva, debe usarla con precaución. Las clases del marco UIKit no están diseñadas para recibir toques que no están vinculados a ellos ... Si desea reenviar toques condicionalmente a otros respondedores en su aplicación, todos estos respondedores deben ser instancias de sus propias subclases de UIView.

Las mejores prácticas de manzanas :

No envíe eventos explícitamente a la cadena de respuesta (a través de nextResponder); en su lugar, invoque la implementación de la superclase y deje que el UIKit maneje el recorrido de la cadena de respuesta.

en su lugar, puede anular:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

en su subclase UIView y devuelva NO si desea que ese toque se envíe a la cadena de respuesta (IE a las vistas detrás de su vista sin nada en él).

Jackslash
fuente
1
Sería increíble tener un enlace a los documentos que está citando, junto con las citas mismas.
morgancodes
1
Agregué
1
~~ ¿Podrías mostrar cómo debería ser esa anulación? ~~ Encontré una respuesta en otra pregunta de cómo se vería esto; está literalmente volviendo NO;
kontur
Probablemente esta debería ser la respuesta aceptada. Es la forma más simple y la recomendada.
Ecuador
8

Una forma mucho más simple es "Desmarcar" la interacción del usuario habilitada en el generador de interfaces. "Si estás usando un guión gráfico"

ingrese la descripción de la imagen aquí

Jeff
fuente
66
Como otros señalaron en una respuesta similar, esto no ayuda en este caso, ya que también deshabilita los eventos táctiles en la vista subyacente. En otras palabras: el botón debajo de esa vista no se puede tocar, independientemente de si activa o desactiva esta configuración.
auco
6

Sobre la base de lo que John publicó, aquí hay un ejemplo que permitirá que los eventos táctiles pasen por todas las subvistas de una vista, excepto los botones:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    // Allow buttons to receive press events.  All other views will get ignored
    for( id foundView in self.subviews )
    {
        if( [foundView isKindOfClass:[UIButton class]] )
        {
            UIButton *foundButton = foundView;

            if( foundButton.isEnabled  &&  !foundButton.hidden &&  [foundButton pointInside:[self convertPoint:point toView:foundButton] withEvent:event] )
                return YES;
        }        
    }
    return NO;
}
Tod Cunningham
fuente
Es necesario verificar si la vista es visible y si las subvistas son visibles.
The Lazy Coder
¡Solución perfecta! Un enfoque aún más simple sería crear una UIViewsubclase PassThroughViewque simplemente se anule -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }si desea reenviar cualquier evento táctil debajo de las vistas.
auco
6

Últimamente escribí una clase que me ayudará con eso. Usarlo como una clase personalizada para 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 una UIButtonque está debajo de un semitransparente, UIViewmientras que la parte no transparente de la UIViewrespuesta seguirá respondiendo a eventos táctiles.

GIF

Como puede ver en el GIF, el botón Giraffe es un rectángulo simple, pero los eventos táctiles en áreas transparentes se pasan al amarillo UIButtondebajo.

Enlace a clase

Segev
fuente
1
Si bien esta solución puede funcionar, es mejor colocar las partes relevantes de su solución dentro de la respuesta.
Koen
4

La solución más votada no funcionaba completamente para mí, supongo que era porque tenía un TabBarController en la jerarquía (como señala uno de los comentarios), de hecho, estaba pasando toques a algunas partes de la interfaz de usuario, pero estaba jugando con mi La capacidad de tableView para interceptar eventos táctiles, lo que finalmente hizo fue anular hitTest en la vista. Quiero ignorar los toques y dejar que las subvistas de esa vista los manejen.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == self) {
        return nil; //avoid delivering touch events to the container view (self)
    }
    else{
        return view; //the subviews will still receive touch events
    }
}
PakitoV
fuente
3

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
}
Barath
fuente
2

De acuerdo con la 'Guía de programación de aplicaciones de iPhone':

Desactivar la entrega de eventos táctiles. De forma predeterminada, una vista recibe eventos táctiles, pero puede establecer su propiedad userInteractionEnabled en NO para desactivar la entrega de eventos.Una vista tampoco recibe eventos si está oculta o si es transparente .

http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/EventHandling/EventHandling.html

Actualizado : ejemplo eliminado - releer la pregunta ...

¿Tiene algún procesamiento de gestos en las vistas que pueden estar procesando los toques antes de que el botón lo obtenga? ¿Funciona el botón cuando no tienes una vista transparente sobre él?

¿Alguna muestra de código de código que no funciona?

LK
fuente
Sí, escribí en mi pregunta que los toques normales funcionan en vistas inferiores, pero los botones UIB y otros elementos no funcionan.
Sean Clark Hess
1

Hasta donde yo sé, se supone que puedes hacer esto anulando el método hitTest . Lo intenté pero no pude hacerlo funcionar correctamente.

Al final, creé una serie de vistas transparentes alrededor del objeto táctil para que no lo cubrieran. Un poco hack para mi problema esto funcionó bien.

Liam
fuente
1

Tomando consejos de las otras respuestas y leyendo la documentación de Apple, creé esta biblioteca simple para resolver su problema:
https://github.com/natrosoft/NATouchThroughView
Hace que sea fácil dibujar vistas en Interface Builder que deberían pasar por toques Una vista subyacente.

Creo que el intercambio de métodos es excesivo y muy peligroso de hacer en el código de producción porque estás jugando directamente con la implementación base de Apple y haciendo un cambio en toda la aplicación que podría causar consecuencias no deseadas.

Hay un proyecto de demostración y es de esperar que el archivo README haga un buen trabajo explicando qué hacer. Para abordar el OP, debe cambiar la clara UIView que contiene los botones para clasificar NATouchThroughView en Interface Builder. Luego, busque la clara UIView que se superpone al menú que desea que se pueda tocar. Cambie esa UIView a la clase NARootTouchThroughView en Interface Builder. Incluso puede ser la UIView raíz de su controlador de vista si desea que esos toques pasen al controlador de vista subyacente. Echa un vistazo al proyecto de demostración para ver cómo funciona. Es realmente bastante simple, seguro y no invasivo.

n8tr
fuente
1

Creé una categoría para hacer esto.

un pequeño método swizzling y la vista es dorada.

El encabezado

//UIView+PassthroughParent.h
@interface UIView (PassthroughParent)

- (BOOL) passthroughParent;
- (void) setPassthroughParent:(BOOL) passthroughParent;

@end

El archivo de implementación

#import "UIView+PassthroughParent.h"

@implementation UIView (PassthroughParent)

+ (void)load{
    Swizz([UIView class], @selector(pointInside:withEvent:), @selector(passthroughPointInside:withEvent:));
}

- (BOOL)passthroughParent{
    NSNumber *passthrough = [self propertyValueForKey:@"passthroughParent"];
    if (passthrough) return passthrough.boolValue;
    return NO;
}
- (void)setPassthroughParent:(BOOL)passthroughParent{
    [self setPropertyValue:[NSNumber numberWithBool:passthroughParent] forKey:@"passthroughParent"];
}

- (BOOL)passthroughPointInside:(CGPoint)point withEvent:(UIEvent *)event{
    // Allow buttons to receive press events.  All other views will get ignored
    if (self.passthroughParent){
        if (self.alpha != 0 && !self.isHidden){
            for( id foundView in self.subviews )
            {
                if ([foundView alpha] != 0 && ![foundView isHidden] && [foundView pointInside:[self convertPoint:point toView:foundView] withEvent:event])
                    return YES;
            }
        }
        return NO;
    }
    else {
        return [self passthroughPointInside:point withEvent:event];// Swizzled
    }
}

@end

Deberá agregar mi Swizz.h y Swizz.m

ubicado aquí

Después de eso, solo importa el UIView + PassthroughParent.h en su archivo {Project} -Prefix.pch, y cada vista tendrá esta capacidad.

cada vista tomará puntos, pero ninguno del espacio en blanco lo hará.

También recomiendo usar un fondo claro.

myView.passthroughParent = YES;
myView.backgroundColor = [UIColor clearColor];

EDITAR

Creé mi propia bolsa de propiedades, y eso no estaba incluido anteriormente.

Archivo de cabecera

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

- (id) propertyValueForKey:(NSString*) key;
- (void) setPropertyValue:(id) value forKey:(NSString*) key;

@end

Archivo de implementacion

// NSObject+PropertyBag.m

#import "NSObject+PropertyBag.h"



@implementation NSObject (PropertyBag)

+ (void) load{
    [self loadPropertyBag];
}

+ (void) loadPropertyBag{
    @autoreleasepool {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Swizz([NSObject class], NSSelectorFromString(@"dealloc"), @selector(propertyBagDealloc));
        });
    }
}

__strong NSMutableDictionary *_propertyBagHolder; // Properties for every class will go in this property bag
- (id) propertyValueForKey:(NSString*) key{
    return [[self propertyBag] valueForKey:key];
}
- (void) setPropertyValue:(id) value forKey:(NSString*) key{
    [[self propertyBag] setValue:value forKey:key];
}
- (NSMutableDictionary*) propertyBag{
    if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
    NSMutableDictionary *propBag = [_propertyBagHolder valueForKey:[[NSString alloc] initWithFormat:@"%p",self]];
    if (propBag == nil){
        propBag = [NSMutableDictionary dictionary];
        [self setPropertyBag:propBag];
    }
    return propBag;
}
- (void) setPropertyBag:(NSDictionary*) propertyBag{
    if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
    [_propertyBagHolder setValue:propertyBag forKey:[[NSString alloc] initWithFormat:@"%p",self]];
}

- (void)propertyBagDealloc{
    [self setPropertyBag:nil];
    [self propertyBagDealloc];//Swizzled
}

@end
El codificador perezoso
fuente
El método passthroughPointInside funciona bien para mí (incluso sin usar ninguno de los elementos passthroughparent o swizz, solo cambie el nombre de passthroughPointInside a pointInside), muchas gracias.
Benjamin Piette
¿Qué es propertyValueForKey?
Skotch
1
Hmm Nadie lo señaló antes. Creé un diccionario de puntero personalizado que contiene propiedades en una extensión de clase. Veré si puedo encontrarlo para incluirlo aquí.
The Lazy Coder
Se sabe que los métodos Swizzling son una solución delicada de piratería para casos raros y diversión para desarrolladores. Definitivamente no es seguro en App Store porque es muy probable que rompa y bloquee su aplicación con la próxima actualización del sistema operativo. ¿Por qué no simplemente anular pointInside: withEvent:?
auco
0

Si no puede molestarse en usar una categoría o subclase de UIView, también puede llevar el botón hacia adelante para que esté frente a la vista transparente. Esto no siempre será posible dependiendo de su aplicación, pero funcionó para mí. Siempre puede volver a traer el botón u ocultarlo.

Sayag sacudido
fuente
2
Hola, bienvenido a desbordar la pila. Solo una pista para usted como nuevo usuario: es posible que desee tener cuidado con su tono. Evitaría frases como "si no puedes molestarte"; puede parecer condescendiente
nomista
Gracias por el aviso. Era más desde un punto de vista perezoso que condescendiente, pero punto tomado.
Sacudió Sayag
0

Prueba esto

class PassthroughToWindowView: UIView {
        override func test(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            var view = super.hitTest(point, with: event)
            if view != self {
                return view
            }

            while !(view is PassthroughWindow) {
                view = view?.superview
            }
            return view
        }
    } 
tBug
fuente
0

Intenta establecer uno backgroundColorde tus transparentViewcomo UIColor(white:0.000, alpha:0.020). Luego puede obtener eventos táctiles en touchesBegan/ touchesMovedmétodos. Coloque el código a continuación en algún lugar donde se inicie su vista:

self.alpha = 1
self.backgroundColor = UIColor(white: 0.0, alpha: 0.02)
self.isMultipleTouchEnabled = true
self.isUserInteractionEnabled = true
Agisight
fuente
0

Lo uso en lugar del punto de método de anulación (dentro: CGPoint, con: UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }
Wei
fuente