Come posso fare clic su un pulsante dietro una UIView trasparente?


177

Supponiamo di avere un controller di visualizzazione con una vista secondaria. la sottoview occupa il centro dello schermo con margini di 100 px su tutti i lati. Aggiungiamo quindi un mucchio di piccole cose su cui fare clic all'interno della sottoview. Stiamo solo usando la vista secondaria per sfruttare il nuovo frame (x = 0, y = 0 all'interno della vista secondaria è in realtà 100.100 nella vista padre).

Quindi, immagina di avere qualcosa dietro la sottoview, come un menu. Voglio che l'utente sia in grado di selezionare una qualsiasi delle "piccole cose" nella sottoview, ma se non c'è nulla lì, voglio che i tocchi passino attraverso di essa (dato che lo sfondo è comunque chiaro) ai pulsanti dietro di essa.

Come posso fare questo? Sembra che tocchi Began, ma i pulsanti non funzionano.


1
Pensavo che le UIVview trasparenti (alpha 0) non dovessero rispondere agli eventi touch?
Evadne Wu,

1
Ho scritto una piccola lezione solo per quello. (Aggiunto un esempio nelle risposte). La soluzione è un po 'migliore della risposta accettata perché è ancora possibile fare clic su una UIButtonsotto semitrasparente UIViewmentre la parte non trasparente UIViewrisponderà comunque agli eventi di tocco.
Segev,

Risposte:


315

Crea una vista personalizzata per il tuo contenitore e sovrascrivi il puntoInside: messaggio per restituire falso quando il punto non rientra in una vista figlio idonea, in questo modo:

Swift:

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
    }
}

Obiettivo C:

@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

L'uso di questa vista come contenitore consentirà a tutti i suoi figli di ricevere tocchi ma la vista stessa sarà trasparente agli eventi.


4
Interessante. Devo tuffarmi di più nella catena dei soccorritori.
Sean Clark Hess,

1
devi verificare se anche la vista è visibile, quindi devi anche controllare se le viste secondarie sono visibili prima di testarle.
The Lazy Coder,

2
sfortunatamente questo trucco non funziona per un tabBarController, per il quale non è possibile modificare la vista. Qualcuno ha un'idea per rendere questa visione trasparente anche agli eventi?
cyrilchampier,

1
Dovresti anche controllare l'alfa della vista. Abbastanza spesso nasconderò una vista impostando l'alfa a zero. Una vista con zero alfa dovrebbe agire come una vista nascosta.
James Andrews,

2
Ho realizzato una versione rapida: forse puoi includerla nella risposta per la visibilità gist.github.com/eyeballz/17945454447c7ae766cb
eyeballz

107

Anche io uso

myView.userInteractionEnabled = NO;

Non è necessario effettuare la sottoclasse. Funziona bene.


76
Ciò disabiliterà anche l'interazione dell'utente per eventuali sottoprogetti
pixelfreak,

Come accennato da @pixelfreak, questa non è la soluzione ideale per tutti i casi. I casi in cui l'interazione nelle sottoview di quella vista sono ancora desiderati richiedono che il flag rimanga abilitato.
Augusto Carmo,

20

Da Apple:

L'inoltro di eventi è una tecnica utilizzata da alcune applicazioni. Inoltri gli eventi tocco invocando i metodi di gestione degli eventi di un altro oggetto responder. Sebbene questa possa essere una tecnica efficace, dovresti usarla con cautela. Le classi del framework UIKit non sono progettate per ricevere tocchi che non sono associati a loro .... Se si desidera inoltrare in modo condizionale tocchi ad altri responder nella propria applicazione, tutti questi risponditori dovrebbero essere istanze delle proprie sottoclassi di UIView.

Best practice sulle mele :

Non inviare esplicitamente eventi nella catena del risponditore (tramite nextResponder); invoca invece l'implementazione della superclasse e lascia che UIKit gestisca l'attraversamento della catena di risposta.

invece puoi sovrascrivere:

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

nella sottoclasse UIView e restituisci NO se desideri che quel tocco venga inviato nella catena del risponditore (IE per le visualizzazioni dietro la tua vista che non contiene nulla).


1
Sarebbe fantastico avere un link ai documenti che stai citando, insieme alle citazioni stesse.
morgancodes,

1
Ho aggiunto i collegamenti ai luoghi pertinenti nella documentazione per te
jackslash,

1
~~ Potresti mostrare come dovrebbe apparire quell'override? ~~ Hai trovato una risposta in un'altra domanda su come sarebbe simile; sta letteralmente solo tornando NO;
Kontur,

Questa dovrebbe probabilmente essere la risposta accettata. È il modo più semplice e consigliato.
Ecuador,

8

Un modo molto più semplice è quello di "Un-Check" Interazione utente abilitata nel builder di interfacce. "Se stai usando uno storyboard"

inserisci qui la descrizione dell'immagine


6
come altri hanno sottolineato in una risposta simile, questo non aiuta in questo caso, poiché disabilita anche gli eventi touch nella vista sottostante. In altre parole: il pulsante sotto quella vista non può essere toccato, indipendentemente dal fatto che tu abbia abilitato o disabilitato questa impostazione.
auco

6

Sulla base di ciò che John ha pubblicato, ecco un esempio che consentirà agli eventi touch di passare attraverso tutte le visualizzazioni secondarie di una vista ad eccezione dei pulsanti:

-(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;
}

È necessario verificare se la vista è visibile e se le viste secondarie sono visibili.
The Lazy Coder,

Soluzione perfetta! Un approccio ancora più semplice sarebbe quello di creare una UIViewsottoclasse PassThroughViewche sovrascriva semplicemente -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }se si desidera inoltrare qualsiasi evento touch sotto le viste.
auco

6

Ultimamente ho scritto un corso che mi aiuterà in questo. Usandolo come una classe personalizzata per un UIButtono UIViewpasserà eventi touch che sono stati eseguiti su un pixel trasparente.

Questa soluzione è in qualche modo migliore della risposta accettata perché è ancora possibile fare clic su una UIButtonsotto semitrasparente UIViewmentre la parte non trasparente UIViewrisponderà comunque agli eventi di tocco.

GIF

Come puoi vedere nella GIF, il pulsante Giraffe è un semplice rettangolo ma gli eventi di tocco su aree trasparenti vengono passati al giallo UIButtonsotto.

Link alla lezione


1
Sebbene questa soluzione possa funzionare, è meglio inserire le parti pertinenti della soluzione nella risposta.
Koen.

4

La soluzione più votata non funzionava completamente per me, suppongo che fosse perché avevo un TabBarController nella gerarchia (come sottolinea uno dei commenti) stava infatti passando tocchi ad alcune parti dell'interfaccia utente ma stava facendo casino con il mio La capacità di tableView di intercettare gli eventi di tocco, quello che alla fine ha fatto è stato scavalcare hitTest nella vista Voglio ignorare i tocchi e lasciare che le subview di quella vista li gestiscano

- (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
    }
}

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
}

2

Secondo la "Guida alla programmazione delle applicazioni iPhone":

Disattivazione della consegna di eventi touch. Per impostazione predefinita, una vista riceve eventi touch, ma è possibile impostare la proprietà userInteractionEnabled su NO per disattivare la consegna degli eventi. Una vista inoltre non riceve eventi se è nascosta o se è trasparente .

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

Aggiornato : esempio rimosso - rileggi la domanda ...

Hai qualche elaborazione gestuale nelle viste che potrebbe elaborare i tocchi prima che il pulsante lo ottenga? Il pulsante funziona quando non hai la vista trasparente su di esso?

Qualche esempio di codice di codice non funzionante?


Sì, ho scritto nella mia domanda che i tocchi normali funzionano sotto le viste, ma gli UIButton e altri elementi non funzionano.
Sean Clark Hess,

1

Per quanto ne so, dovresti essere in grado di farlo ignorando il metodo hitTest : . L'ho provato ma non sono riuscito a farlo funzionare correttamente.

Alla fine ho creato una serie di viste trasparenti attorno all'oggetto toccabile in modo che non lo coprissero. Un po 'di hack per il mio problema ha funzionato bene.


1

Prendendo suggerimenti dalle altre risposte e leggendo la documentazione di Apple, ho creato questa semplice libreria per risolvere il tuo problema:
https://github.com/natrosoft/NATouchThroughView
Rende facile disegnare viste in Interface Builder che dovrebbero passare i tocchi attraverso una visione di base.

Penso che il metodo di swizzling sia eccessivo e molto pericoloso da fare nel codice di produzione perché stai scherzando direttamente con l'implementazione di base di Apple e stai apportando una modifica a livello di applicazione che potrebbe causare conseguenze indesiderate.

C'è un progetto dimostrativo e speriamo che il README faccia un buon lavoro spiegando cosa fare. Per indirizzare l'OP, è necessario modificare l'UIView chiaro che contiene i pulsanti nella classe NATouchThroughView in Interface Builder. Quindi trova UIView chiaro che si sovrappone al menu che desideri abilitare al tocco. Cambia l'UIView nella classe NARootTouchThroughView in Interface Builder. Può persino essere l'UIView principale del controller della vista se si desidera che quei tocchi passino al controller della vista sottostante. Dai un'occhiata al progetto demo per vedere come funziona. È davvero abbastanza semplice, sicuro e non invasivo


1

Ho creato una categoria per farlo.

un piccolo metodo sfrigolante e la vista è d'oro.

L'intestazione

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

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

@end

Il file di implementazione

#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

Dovrai aggiungere i miei Swizz.h e Swizz.m

situato qui

Successivamente, devi solo importare UIView + PassthroughParent.h nel tuo file {Project} -Prefix.pch e ogni vista avrà questa capacità.

ogni vista prenderà punti, ma nessuno spazio vuoto lo farà.

Consiglio anche di usare uno sfondo chiaro.

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

MODIFICARE

Ho creato la mia borsa di proprietà, che non era inclusa in precedenza.

File di intestazione

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

File di implementazione

// 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

Il metodo passthroughPointInside funziona bene per me (anche senza usare nessuna delle cose passthroughparent o swizz - basta rinominare passthroughPointInside in pointInside), grazie mille.
Benjamin Piette,

Che cos'è propertyValueForKey?
Skotch,

1
Hmm. Nessuno lo ha sottolineato prima. Ho creato un dizionario puntatore personalizzato che contiene proprietà in un'estensione di classe. Vedrò se riesco a trovarlo per includerlo qui.
The Lazy Coder,

I metodi Swizzling sono noti per essere una soluzione di hacking delicata per casi rari e divertimento per gli sviluppatori. Non è sicuramente App Store sicuro perché è molto probabile che si rompa e blocchi l'app con il prossimo aggiornamento del sistema operativo. Perché non semplicemente ignorare pointInside: withEvent:?
auco

0

Se non ti preoccupi di usare una UIView di categoria o sottoclasse, puoi anche portare avanti il ​​pulsante in modo che sia davanti alla vista trasparente. Questo non sarà sempre possibile a seconda dell'applicazione, ma ha funzionato per me. Puoi sempre riportare nuovamente il pulsante o nasconderlo.


2
Ciao, benvenuto nello stack di overflow. Solo un suggerimento per te come nuovo utente: potresti voler stare attento al tuo tono. Eviterei frasi come "se non puoi disturbare"; potrebbe apparire condiscendente
nomistico

Grazie per il testa a testa .. Era più da un punto di vista pigro che condiscendente, ma punto preso.
Agitato Sayag

0

Prova questo

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
        }
    } 

0

Prova a impostare un backgroundColortuo transparentViewas UIColor(white:0.000, alpha:0.020). Quindi è possibile ottenere eventi tocco in touchesBegan/ touchesMovedmetodi. Posiziona il codice in basso da qualche parte in cui viene visualizzata la tua vista:

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

0

Lo uso al posto del metodo override point (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
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.