Catturare tocchi su una sottoview al di fuori della cornice della sua superview usando hitTest: withEvent:


94

Il mio problema: ho una superview EditViewche occupa praticamente l'intero frame dell'applicazione e una sottoview MenuViewche occupa solo il 20% inferiore, e quindi MenuViewcontiene la propria sottoview ButtonViewche risiede effettivamente al di fuori dei MenuViewlimiti di s (qualcosa del genere :) ButtonView.frame.origin.y = -100.

(nota: EditViewha altre viste secondarie che non fanno parte della MenuViewgerarchia delle viste di, ma possono influire sulla risposta.)

Probabilmente conosci già il problema: quando si ButtonViewtrova nei limiti di MenuView(o, più specificamente, quando i miei tocchi sono entro MenuViewi limiti), ButtonViewrisponde agli eventi di tocco. Quando i miei tocchi sono fuori MenuViewdai limiti di (ma ancora entro ButtonViewi limiti di), nessun evento di tocco viene ricevuto da ButtonView.

Esempio:

  • (E) è EditViewil genitore di tutte le visualizzazioni
  • (M) è MenuViewuna sottoview di EditView
  • (B) è ButtonViewuna sottoview di MenuView

Diagramma:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Poiché (B) è al di fuori della cornice di (M), un tocco nella regione (B) non verrà mai inviato a (M) - in effetti, (M) non analizza mai il tocco in questo caso, e il tocco viene inviato a il prossimo oggetto nella gerarchia.

Obiettivo: immagino che l'override hitTest:withEvent:possa risolvere questo problema, ma non capisco esattamente come. Nel mio caso, dovrebbe hitTest:withEvent:essere sovrascritto in EditView(la mia superview "principale")? O dovrebbe essere sovrascritto nella visualizzazione MenuViewdiretta del pulsante che non riceve i tocchi? O sto pensando a questo in modo errato?

Se ciò richiede una lunga spiegazione, una buona risorsa online sarebbe utile, ad eccezione dei documenti UIView di Apple, che non mi hanno chiarito.

Grazie!

Risposte:


145

Ho modificato il codice della risposta accettata in modo che sia più generico: gestisce i casi in cui la vista ritaglia le sottoview fino ai suoi limiti, può essere nascosta e, cosa più importante: se le sottoview sono gerarchie di viste complesse, verrà restituita la sottoview corretta.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.clipsToBounds) {
        return nil;
    }

    if (self.hidden) {
        return nil;
    }

    if (self.alpha == 0) {
        return nil;
    }

    for (UIView *subview in self.subviews.reverseObjectEnumerator) {
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        UIView *result = [subview hitTest:subPoint withEvent:event];

        if (result) {
            return result;
        }
    }

    return nil;
}

SWIFT 3

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

    if clipsToBounds || isHidden || alpha == 0 {
        return nil
    }

    for subview in subviews.reversed() {
        let subPoint = subview.convert(point, from: self)
        if let result = subview.hitTest(subPoint, with: event) {
            return result
        }
    }

    return nil
}

Spero che questo aiuti chiunque cerchi di utilizzare questa soluzione per casi d'uso più complessi.


Sembra buono e grazie per averlo fatto. Tuttavia, avevo una domanda: perché stai facendo return [super hitTest: point withEvent: event]; ? Non restituiresti semplicemente zero se non ci sono sottoview che attivano il tocco? Apple dice che hitTest restituisce zero se nessuna sottoview contiene il tocco.
Ser Pounce

Hm ... Sì, sembra proprio giusto. Inoltre, per un comportamento corretto, gli oggetti devono essere iterati in ordine inverso (perché l'ultimo è quello più in alto visivamente). Codice modificato in modo che corrisponda.
Noam

3
Ho appena usato la tua soluzione per fare in modo che un UIButton catturi il tocco, ed è all'interno di un UIView che si trova all'interno di un UICollectionViewCell all'interno di (ovviamente) un UICollectionView. Ho dovuto sottoclassare UICollectionView e UICollectionViewCell per sovrascrivere hitTest: withEvent: su queste tre classi. E funziona come un fascino !! Grazie !!
Daniel García

3
A seconda dell'uso desiderato, dovrebbe restituire [super hitTest: point withEvent: event] o nil. Restituire se stesso gli farebbe ricevere tutto.
Noam

1
Ecco un documento tecnico di domande e risposte di Apple su questa stessa tecnica: developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang

33

Ok, ho fatto degli scavi e dei test, ecco come hitTest:withEventfunziona, almeno ad alto livello. Immagina questo scenario:

  • (E) è EditView , il genitore di tutte le visualizzazioni
  • (M) è MenuView , una sottoview di EditView
  • (B) è ButtonView , una sottoview di MenuView

Diagramma:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Poiché (B) è al di fuori della cornice di (M), un tocco nella regione (B) non verrà mai inviato a (M) - in effetti, (M) non analizza mai il tocco in questo caso, e il tocco viene inviato a il prossimo oggetto nella gerarchia.

Tuttavia, se si implementa hitTest:withEvent:in (M), i tocchi in qualsiasi punto dell'applicazione verranno inviati a (M) (o almeno li conosce). Puoi scrivere il codice per gestire il tocco in quel caso e restituire l'oggetto che dovrebbe ricevere il tocco.

Più specificamente: l'obiettivo di hitTest:withEvent:è restituire l'oggetto che dovrebbe ricevere il colpo. Quindi, in (M) potresti scrivere codice come questo:

// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
    for (UIView *subview in self.subviews) {
        if (CGRectContainsPoint(subview.frame, point)) {
            return subview;
        }
    }

    // use this to pass the 'touch' onward in case no subviews trigger the touch
    return [super hitTest:point withEvent:event];
}

Sono ancora molto nuovo a questo metodo e questo problema, quindi se ci sono modi più efficienti o corretti per scrivere il codice, per favore commenta.

Spero che questo aiuti chiunque altro risponderà a questa domanda in seguito. :)


Grazie, questo ha funzionato per me, anche se ho dovuto fare un po 'più di logica per determinare quale delle visualizzazioni secondarie dovrebbe effettivamente ricevere risultati. Ho rimosso una pausa non necessaria dal tuo esempio btw.
Daniel Saidi

Quando il frame della vista secondaria era oltre il frame della vista principale, l'evento clic non rispondeva! La tua soluzione ha risolto questo problema. Grazie mille :-)
diJeevan

@toblerpwn (soprannome divertente :)) dovresti modificare questa eccellente risposta precedente per rendere molto chiaro (USA GRANDI MAIUSCOLI IN GRASSETTO) in quale classe devi aggiungere questo. Saluti!
Fattie

26

In Swift 5

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
    for member in subviews.reversed() {
        let subPoint = member.convert(point, from: self)
        guard let result = member.hitTest(subPoint, with: event) else { continue }
        return result
    }
    return nil
}

Funzionerà solo se applichi l'override alla superview diretta della vista "comportamento anomalo"
Hudi Ilfeld,

2

Quello che farei è che sia ButtonView che MenuView esistano allo stesso livello nella gerarchia della vista posizionandoli entrambi in un contenitore la cui cornice si adatta perfettamente a entrambi. In questo modo la regione interattiva dell'elemento ritagliato non verrà ignorata a causa dei confini della sua supervisualizzazione.


Ho pensato anche a questa soluzione alternativa - significa che dovrò duplicare una logica di posizionamento (o rifattorizzare un codice serio!), Ma alla fine potrebbe essere la mia scelta migliore ..
toblerpwn

1

Se hai molte altre viste secondarie all'interno della tua vista genitore, probabilmente la maggior parte delle altre viste interattive non funzionerebbe se usi le soluzioni di cui sopra, in tal caso puoi usare qualcosa del genere (in Swift 3.2):

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}

0

Se qualcuno ne ha bisogno, ecco l'alternativa rapida

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
        for subview in self.subviews.reverse() {
            let subPoint = subview.convertPoint(point, fromView:self);

            if let result = subview.hitTest(subPoint, withEvent:event) {
                return result;
            }
        }
    }

    return nil
}

0

Inserisci le seguenti righe di codice nella tua gerarchia di visualizzazione:

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

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

Per ulteriori chiarimenti, è stato spiegato nel mio blog: "goaheadwithiphonetech" riguardo a "Callout personalizzato: il pulsante non è un problema cliccabile".

Spero che ti aiuti ... !!!


Il tuo blog è stato rimosso, quindi dove possiamo trovare la spiegazione per favore?
ishahak
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.