Consentire l'interazione con una UIView sotto un'altra UIView


115

Esiste un modo semplice per consentire l'interazione con un pulsante in una UIView che si trova sotto un'altra UIView - dove non ci sono oggetti reali dalla UIView in alto sopra il pulsante?

Ad esempio, al momento ho una UIView (A) con un oggetto in alto e un oggetto in fondo allo schermo e niente nel mezzo. Questo si trova in cima a un'altra UIView che ha i pulsanti al centro (B). Tuttavia, non riesco a interagire con i pulsanti al centro di B.

Riesco a vedere i pulsanti in B - ho impostato lo sfondo di A su clearColor - ma i pulsanti in B non sembrano ricevere tocchi nonostante il fatto che non ci siano oggetti da A effettivamente sopra quei pulsanti.

MODIFICA - Voglio ancora essere in grado di interagire con gli oggetti nella UIView in alto

Sicuramente c'è un modo semplice per farlo?


2
È praticamente tutto spiegato qui: developer.apple.com/iphone/library/documentation/iPhone/… Ma fondamentalmente, sovrascrivi hitTest: withEvent :, forniscono anche un esempio di codice.
nash

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

Risposte:


97

È necessario creare una sottoclasse UIView per la vista dall'alto e sostituire il metodo seguente:

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

Puoi anche guardare il metodo hitTest: event:.


19
Cosa rappresenta middle_y1 / y2 qui?
Jason Renaldo

Non sono sicuro di cosa returnstia facendo quella dichiarazione, ma return CGRectContainsPoint(eachSubview.frame, point)funziona per me. Risposta estremamente utile altrimenti
n00neimp0rtant

Il business MIDDLE_Y1 / Y2 è solo un esempio. Questa funzione sarà "trasparente" per eventi touch MIDDLE_Y1<=y<=MIDDLE_Y2nell'area.
gyim

41

Sebbene molte delle risposte qui funzionino, sono un po 'sorpreso di vedere che la risposta più conveniente, generica e infallibile non è stata fornita qui. @Ash si è avvicinato di più, tranne per il fatto che sta succedendo qualcosa di strano con la restituzione della superview ... non farlo.

Questa risposta è presa da una risposta che ho dato a una domanda simile, qui .

- (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]restituirà la vista più profonda nella gerarchia di quella vista che è stata toccata. Se hitView == self(cioè se non c'è una sottoview sotto il punto di contatto), torna nil, specificando che questa vista non dovrebbe ricevere il tocco. Il modo in cui funziona la catena di risponditori significa che la gerarchia della vista sopra questo punto continuerà ad essere attraversata finché non viene trovata una vista che risponderà al tocco. Non restituire la superview, poiché non dipende da questa vista se la sua superview deve accettare o meno i tocchi!

Questa soluzione è:

  • conveniente , perché non richiede riferimenti ad altre viste / sottoviste / oggetti;
  • generico , perché si applica a qualsiasi vista che funge puramente da contenitore per le sottoview touchable e la configurazione delle sottoview non influisce sul modo in cui funziona (come succede se si sovrascrive pointInside:withEvent:per restituire una particolare area touchable).
  • infallibile , non c'è molto codice ... e il concetto non è difficile da capire.

Lo uso abbastanza spesso da averlo astratto in una sottoclasse per salvare sottoclassi di vista inutili per una sostituzione. Come bonus, aggiungi una proprietà per renderla configurabile:

@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

Quindi scatenati e usa questa vista ovunque tu possa usare una pianura UIView. Configurarlo è semplice come impostare onlyRespondToTouchesInSubviewssu YES.


2
L'unica risposta corretta. per essere chiari, se vuoi ignorare i tocchi di una vista, ma non dei pulsanti (diciamo) contenuti nella vista , fai come spiega Stuart. (Io di solito lo chiamo "vista titolare" perché può "tenere premuto" in modo innocuo alcuni pulsanti, ma non influisce su nulla "sotto" il titolare.)
Fattie

Alcune delle altre soluzioni che utilizzano i punti hanno problemi se la tua vista è scorrevole, come UITableView o UICollectionView e hai fatto scorrere su o giù. Questa soluzione, tuttavia, funziona indipendentemente dallo scorrimento.
pajevic

31

Ci sono diversi modi per gestirlo. Il mio preferito è sovrascrivere hitTest: withEvent: in una vista che è una superview comune (forse indirettamente) alle viste in conflitto (sembra che tu chiami queste A e B). Ad esempio, qualcosa del genere (qui A e B sono puntatori UIView, dove B è quello "nascosto", che normalmente viene ignorato):

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

Puoi anche modificare il pointInside:withEvent:metodo come suggerito da gyim. Ciò consente di ottenere essenzialmente lo stesso risultato "colpendo un buco" in A, almeno per i tocchi.

Un altro approccio è l'inoltro di eventi, che significa sovrascrivere touchesBegan:withEvent:metodi simili (come touchesMoved:withEvent:ecc.) Per inviare alcuni tocchi a un oggetto diverso da quello in cui si trovano prima. Ad esempio, in A, potresti scrivere qualcosa del genere:

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

Tuttavia, questo non funzionerà sempre come ti aspetti! La cosa principale è che i controlli incorporati come UIButton ignoreranno sempre i tocchi inoltrati. Per questo motivo, il primo approccio è più affidabile.

C'è un buon post sul blog che spiega tutto questo in modo più dettagliato, insieme a un piccolo progetto xcode funzionante per dimostrare le idee, disponibile qui:

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


Il problema con questa soluzione - sovrascrivere hitTest sulla superview comune - è che non consente alla vista sottostante di funzionare completamente correttamente quando la vista sottostante fa parte di un intero insieme di viste scorrevoli (MapView ecc.). Punto di sovrascrittura All'interno della vista dall'alto come suggerito da gyim di seguito funziona in tutti i casi per quanto ne so.
delany

@delany, non è vero; puoi avere viste scorrevoli che si trovano sotto altre viste e farle funzionare entrambe sovrascrivendo hitTest. Ecco un po 'di codice di esempio: bynomial.com/blogfiles/Temp32.zip
Tyler

Hey. Non tutte le visualizzazioni scorrevoli - solo alcune ... Ho provato il tuo codice postale dall'alto, cambiando UIScrollView in un (ad esempio) MKMapView e non funziona. I tocchi funzionano: è lo scorrimento che sembra essere il problema.
delany

Ok, l'ho verificato e ho confermato che hitTest non funziona come vorresti con MKMapView. Avevi ragione su questo, @delany; sebbene funzioni correttamente con UIScrollView. Mi chiedo perché MKMapView fallisce?
Tyler

@Tyler Come hai detto, che "La cosa principale è che i controlli incorporati come UIButton ignoreranno sempre i tocchi inoltrati", voglio sapere come lo sai e se è un documento ufficiale per spiegare questo comportamento. Ho riscontrato un problema per cui quando UIButton ha una semplice sottoview UIView, non risponderà agli eventi di tocco nei limiti della sottoview. E ho scoperto che l'evento viene inoltrato correttamente al pulsante UIB come comportamento predefinito di UIView, ma non sono sicuro che sia una funzionalità designata o solo un bug. Potresti mostrarmi la documentazione a riguardo? Grazie mille sinceramente.
Neal.Marlin

29

Devi impostare upperView.userInteractionEnabled = NO;, altrimenti la vista superiore intercetterà i tocchi.

La versione Interface Builder di questo è una casella di controllo nella parte inferiore del pannello Visualizza attributi denominata "Interazione utente abilitata". Deselezionalo e dovresti essere a posto.


Scusa, avrei dovuto dirlo. Voglio comunque essere in grado di interagire con gli oggetti nella UIView superiore.
delany

Ma la vista superiore non può ricevere alcun tocco, includere il pulsante nella vista superiore.
imcaptor

2
Questa soluzione funziona per me. Non avevo bisogno della visuale superiore per reagire ai tocchi.
TJ

11

L'implementazione personalizzata di pointInside: withEvent: sembrava davvero la strada da percorrere, ma trattare con coordinate hard-coded mi sembrava strano. Quindi ho finito per verificare se il CGPoint era all'interno del pulsante CGRect utilizzando la funzione CGRectContainsPoint ():

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

8

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

Questa soluzione è in qualche modo migliore rispetto alla risposta accettata perché puoi ancora fare clic su un UIButtonche è sotto un semitrasparente UIViewmentre la parte non trasparente di UIViewrisponderà comunque agli eventi di tocco.

GIF

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

Collegamento alla classe


2
Poiché il tuo codice non è così lungo, dovresti includerne le parti pertinenti nella risposta, nel caso in cui il tuo progetto venga spostato o rimosso in futuro.
Gavin

Grazie, la tua soluzione funziona per il mio caso in cui ho bisogno della parte non trasparente della mia UIView per rispondere agli eventi di tocco, mentre la parte trasparente no. Brillante!
Bruce

@Bruce Sono contento che ti abbia aiutato!
Segev

4

Immagino di essere un po 'in ritardo a questa festa, ma aggiungerò questa possibile soluzione:

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

Se usi questo codice per sovrascrivere la funzione hitTest standard di UIView personalizzata, ignorerà SOLO la vista stessa. Tutte le visualizzazioni secondarie di quella vista restituiranno normalmente i loro hit e qualsiasi hit che sarebbe andato alla visualizzazione stessa verrà passato alla sua superview.

-Cenere


1
questo è il mio metodo preferito tranne, non penso che dovresti tornare [self superview]. la documentazione su questo metodo dichiara "Restituisce il discendente più lontano del ricevitore nella gerarchia di visualizzazione (incluso se stesso) che contiene un punto specificato" e "Restituisce zero se il punto si trova completamente al di fuori della gerarchia di visualizzazione del ricevitore". Penso che dovresti tornare nil. quando ritorni a zero, il controllo passerà alla superview per verificare se ha hit o meno. quindi fondamentalmente farà la stessa cosa, tranne per il fatto che la restituzione della superview potrebbe rompere qualcosa in futuro.
jasongregori

Certo, probabilmente sarebbe saggio (annota la data sulla mia risposta originale - da allora ho fatto molto più codice)
Ash

4

Sto solo riffando la risposta accettata e inserendola qui come riferimento. La risposta accettata funziona perfettamente. Puoi estenderlo in questo modo per consentire alle viste secondarie della tua vista di ricevere il tocco, OPPURE trasmetterlo a qualsiasi vista dietro di noi:

- (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: non devi nemmeno fare la ricorsione sull'albero delle sottoview, perché ogni pointInside:withEvent:metodo lo gestirà per te.


3

L'impostazione della proprietà userInteraction disabilitata potrebbe aiutare. Per esempio:

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

(Nota: nel codice precedente, "self" si riferisce a una vista)

In questo modo, puoi visualizzare solo su topView, ma non otterrai input dell'utente. Tutti quei tocchi dell'utente passeranno attraverso questa vista e la vista dal basso risponderà per loro. Userei questo topView per visualizzare immagini trasparenti o per animarle.


3

Questo approccio è abbastanza pulito e consente che anche le visualizzazioni secondarie trasparenti non reagiscano ai tocchi. Basta sottoclasse UIViewe aggiungere il seguente metodo alla sua implementazione:

@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

La mia soluzione qui:

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

Spero che questo ti aiuti


2

C'è qualcosa che puoi fare per intercettare il tocco in entrambe le visualizzazioni.

Vista dall'alto:

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

Ma questa è l'idea.


Questo è certamente possibile, ma è un sacco di lavoro (dovresti rotolare i tuoi oggetti sensibili al tocco nello strato inferiore (ad esempio i pulsanti), penso?) E sembra strano che uno debba rotolare il tuo in questo modo per ottenere comportamenti che sembrerebbero essere intuitivi.
delany

Non sono sicuro che forse dovremmo solo fare un tentativo.
Alexandre Cassagne

2

Ecco una versione 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

Non ho mai creato un'interfaccia utente completa utilizzando il toolkit dell'interfaccia utente, quindi non ho molta esperienza con esso. Ecco cosa penso dovrebbe funzionare però.

Ogni UIView, e questa UIWindow, ha una proprietà subviews, che è un NSArray contenente tutte le sottoview.

La prima vista secondaria che aggiungi a una vista riceverà l'indice 0, la successiva indice 1 e così via. Puoi anche sostituire addSubview:con insertSubview: atIndex:o insertSubview:aboveSubview:e tali metodi che possono determinare la posizione della tua sottoview nella gerarchia.

Quindi controlla il tuo codice per vedere quale visualizzazione aggiungi per prima alla tua UIWindow. Questo sarà 0, l'altro sarà 1.
Ora, da una delle tue visualizzazioni secondarie, per raggiungerne un'altra, devi fare quanto segue:

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

Fammi sapere se funziona per il tuo caso!


(sotto questo indicatore c'è la mia risposta precedente):

Se le viste devono comunicare tra loro, dovrebbero farlo tramite un controller (ovvero, utilizzando il popolare modello MVC ).

Quando crei una nuova vista, puoi assicurarti che si registri con un controller.

Quindi la tecnica è assicurarti che le tue viste registrino con un controller (che può memorizzarle per nome o come preferisci in un dizionario o array). Puoi fare in modo che il controller invii un messaggio per te oppure puoi ottenere un riferimento alla vista e comunicare direttamente con essa.

Se la tua vista non ha un collegamento al controller (che potrebbe essere il caso), puoi utilizzare singleton e / o metodi di classe per ottenere un riferimento al tuo controller.


Grazie per la tua risposta, ma non sono sicuro di aver capito. Entrambe le viste hanno un controller: il problema è che, con una vista sopra l'altra, la vista dal basso non raccoglie eventi (e li inoltra al suo controller), anche se non c'è alcun oggetto che effettivamente 'blocchi' quegli eventi in la vista dall'alto.
delany

Hai una UIWindow nella parte superiore delle nostre UIVview? Se lo fai, gli eventi dovrebbero essere propagati e non dovresti aver bisogno di fare alcuna "magia". Dai un'occhiata a [Window and Views] [1] all'Apple Dev Center (e con tutti i mezzi, aggiungi un altro commento se questo non ti aiuta!) [1]: developer.apple.com/iphone/library/documentation/ iPhone / ...
nash

Sì, assolutamente: una UIWindow in cima alla gerarchia.
delany

Ho aggiornato la mia risposta per includere il codice per passare attraverso una gerarchia di visualizzazioni. Fammi sapere se hai bisogno di altro aiuto!
Nash

Grazie per il tuo aiuto nash, ma ho abbastanza esperienza nella creazione di queste interfacce e quella attuale è configurata correttamente per quanto ne so. Il problema sembra essere il comportamento predefinito delle visualizzazioni complete quando vengono posizionate sopra le altre.
delany

1

Penso che il modo giusto sia usare la catena di visualizzazioni incorporata nella gerarchia delle visualizzazioni. Per le tue viste secondarie che vengono inviate alla vista principale, non utilizzare la UIView generica, ma invece la sottoclasse UIView (o una delle sue varianti come UIImageView) per creare MYView: UIView (o qualsiasi supertipo desiderato, come UIImageView). Nell'implementazione di YourView, implementare il metodo touchesBegan. Questo metodo verrà quindi richiamato quando viene toccata quella vista. Tutto ciò che devi avere in quell'implementazione è un metodo di istanza:

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

questo touchesBegan è un responder api, quindi non è necessario dichiararlo nella tua interfaccia pubblica o privata; è una di quelle API magiche che devi conoscere. Questo self.superview farà apparire la richiesta alla fine al viewController. Nel viewController, quindi, implementa questo touchesBegan per gestire il tocco.

Si noti che la posizione dei tocchi (CGPoint) viene regolata automaticamente in relazione alla vista che lo comprende quando viene rimbalzato lungo la catena gerarchica della vista.


1

Voglio solo pubblicare questo, perché ho avuto un problema in qualche modo simile, ho trascorso una notevole quantità di tempo cercando di implementare le risposte qui senza fortuna. Quello che ho finito per fare:

 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 dal basso era un controller di navigazione con un numero di passaggi e sopra avevo una specie di porta che poteva chiudersi con un gesto di panoramica. L'intera cosa era incorporata in un altro VC. Ha funzionato come un fascino. Spero che questo ti aiuti.


1

Implementazione di Swift 4 per la soluzione basata su HitTest

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

0

Derivato dalla risposta eccellente e per lo più infallibile di Stuart, e dall'utile implementazione di Segev, ecco un pacchetto Swift 4 che puoi inserire in qualsiasi progetto:

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

E poi 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)
}
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.