Erfassen von Berührungen einer Unteransicht außerhalb des Rahmens der Übersicht mit hitTest: withEvent:

94

Mein Problem: Ich habe eine Übersicht EditView, die im Grunde den gesamten Anwendungsrahmen einnimmt, und eine Unteransicht, MenuViewdie nur die unteren ~ 20% MenuVieweinnimmt und dann eine eigene Unteransicht enthält, ButtonViewdie sich tatsächlich außerhalb der MenuViewGrenzen befindet (so etwas wie :) ButtonView.frame.origin.y = -100.

(Hinweis: EditViewEnthält andere Unteransichten, die nicht Teil der MenuViewAnsichtshierarchie sind, aber die Antwort beeinflussen können.)

Sie kennen das Problem wahrscheinlich bereits: Wann ButtonViewinnerhalb der Grenzen von MenuView(oder genauer gesagt, wenn meine Berührungen innerhalb MenuViewder Grenzen liegen), ButtonViewreagiert auf Berührungsereignisse. Wenn meine Berührungen außerhalb der MenuViewGrenzen liegen (aber immer noch innerhalb ButtonViewder Grenzen), wird kein Berührungsereignis von empfangen ButtonView.

Beispiel:

  • (E) ist EditViewdas übergeordnete Element aller Ansichten
  • (M) ist MenuVieweine Unteransicht von EditView
  • (B) ist ButtonVieweine Unteransicht von MenuView

Diagramm:

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

Da sich (B) außerhalb des Rahmens von (M) befindet, wird niemals ein Tippen in der Region (B) an (M) gesendet. Tatsächlich analysiert (M) in diesem Fall niemals die Berührung, und die Berührung wird an gesendet das nächste Objekt in der Hierarchie.

Ziel: Ich hitTest:withEvent:verstehe , dass das Überschreiben dieses Problem lösen kann, aber ich verstehe nicht genau, wie. Sollte in meinem Fall hitTest:withEvent:in EditView(meine 'Master'-Übersicht) überschrieben werden? Oder sollte es in MenuViewder direkten Übersicht über die Schaltfläche, die keine Berührungen erhält , überschrieben werden ? Oder denke ich falsch darüber nach?

Wenn dies eine ausführliche Erklärung erfordert, wäre eine gute Online-Ressource hilfreich - mit Ausnahme der UIView-Dokumente von Apple, die es mir nicht klar gemacht haben.

Vielen Dank!

toblerpwn
quelle

Antworten:

145

Ich habe den Code der akzeptierten Antwort so geändert, dass er allgemeiner ist. Er behandelt die Fälle, in denen die Ansicht Unteransichten an ihre Grenzen schneidet, möglicherweise ausgeblendet ist und, was noch wichtiger ist: Wenn die Unteransichten komplexe Ansichtshierarchien sind, wird die richtige Unteransicht zurückgegeben.

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

Ich hoffe, dies hilft jedem, der versucht, diese Lösung für komplexere Anwendungsfälle zu verwenden.

Noam
quelle
Das sieht gut aus und danke dafür. Hatte jedoch eine Frage: Warum machst du return [super hitTest: point withEvent: event]; ? Würden Sie nicht einfach Null zurückgeben, wenn es keine Unteransicht gibt, die die Berührung auslöst? Laut Apple gibt hitTest null zurück, wenn keine Unteransicht die Berührung enthält.
Ser Pounce
Hm ... Ja, das klingt genau richtig. Für ein korrektes Verhalten müssen die Objekte außerdem in umgekehrter Reihenfolge iteriert werden (da das letzte visuell das oberste ist). Bearbeitender Code passend.
Noam
3
Ich habe gerade Ihre Lösung verwendet, um einen UIButton dazu zu bringen, die Berührung zu erfassen, und er befindet sich in einer UIView, die sich in einer UICollectionViewCell innerhalb (offensichtlich) einer UICollectionView befindet. Ich musste UICollectionView und UICollectionViewCell in Unterklassen unterteilen, um hitTest: withEvent: für diese drei Klassen zu überschreiben. Und es funktioniert wie Charme !! Vielen Dank !!
Daniel García
3
Abhängig von der gewünschten Verwendung sollte entweder [super hitTest: point withEvent: event] oder nil zurückgegeben werden. Die Rückkehr des Selbst würde dazu führen, dass es alles empfängt.
Noam
1
Hier ist ein technisches Dokument mit Fragen und Antworten von Apple zu dieser Technik: developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang
33

Ok, ich habe ein bisschen gegraben und getestet, so hitTest:withEventfunktioniert es - zumindest auf hohem Niveau. Stellen Sie sich dieses Szenario vor:

  • (E) ist EditView , das übergeordnete Element aller Ansichten
  • (M) ist MenuView , eine Unteransicht von EditView
  • (B) ist ButtonView , eine Unteransicht von MenuView

Diagramm:

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

Da sich (B) außerhalb des Rahmens von (M) befindet, wird niemals ein Tippen in der Region (B) an (M) gesendet. Tatsächlich analysiert (M) in diesem Fall niemals die Berührung, und die Berührung wird an gesendet das nächste Objekt in der Hierarchie.

Wenn Sie jedoch hitTest:withEvent:in (M) implementieren , werden Abgriffe an einer beliebigen Stelle in der Anwendung an (M) gesendet (oder es weiß zumindest davon). In diesem Fall können Sie Code schreiben, um die Berührung zu verarbeiten, und das Objekt zurückgeben, das die Berührung erhalten soll.

Genauer gesagt: Das Ziel von hitTest:withEvent:ist es, das Objekt zurückzugeben, das den Treffer erhalten soll. In (M) könnten Sie also Code wie folgt schreiben:

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

Ich bin noch sehr neu in dieser Methode und diesem Problem. Wenn es also effizientere oder korrektere Möglichkeiten gibt, den Code zu schreiben, kommentieren Sie dies bitte.

Ich hoffe, das hilft allen anderen, die diese Frage später beantworten. :) :)

toblerpwn
quelle
Danke, das hat bei mir funktioniert, obwohl ich etwas mehr Logik machen musste, um zu bestimmen, welche der Unteransichten tatsächlich Treffer erhalten sollte. Ich habe übrigens eine unnötige Pause aus Ihrem Beispiel entfernt.
Daniel Saidi
Wenn sich der Unteransichtsrahmen außerhalb des Rahmens der übergeordneten Ansicht befand, reagierte das Klickereignis nicht! Ihre Lösung hat dieses Problem behoben. Vielen Dank :-)
vonJeevan
@toblerpwn (lustiger Spitzname :)) Sie sollten diese ausgezeichnete ältere Antwort bearbeiten, um deutlich zu machen (USE LARGE BOLD CAPS), in welcher Klasse Sie diese hinzufügen müssen. Prost!
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
}
duan
quelle
Dies funktioniert nur, wenn Sie die Überschreibung auf die direkte Übersicht der Ansicht "Fehlverhalten" anwenden
Hudi Ilfeld
2

Was ich tun würde, ist, dass sowohl ButtonView als auch MenuView auf derselben Ebene in der Ansichtshierarchie vorhanden sind, indem beide in einem Container platziert werden, dessen Rahmen vollständig zu beiden passt. Auf diese Weise wird der interaktive Bereich des abgeschnittenen Elements aufgrund der Grenzen seiner Übersicht nicht ignoriert.

mamackenzie
quelle
Ich habe auch über diese Problemumgehung nachgedacht - es bedeutet, dass ich eine Platzierungslogik duplizieren muss (oder einen seriösen Code umgestalten muss!), aber es kann am Ende tatsächlich meine beste Wahl sein.
toblerpwn
1

Wenn Sie viele andere Unteransichten in Ihrer übergeordneten Ansicht haben, funktionieren wahrscheinlich die meisten anderen interaktiven Ansichten nicht, wenn Sie die oben genannten Lösungen verwenden. In diesem Fall können Sie Folgendes verwenden (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)
    }
}
Paras Gupta
quelle
0

Wenn jemand es braucht, ist hier die schnelle Alternative

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
}
Sonny
quelle
0

Platzieren Sie die folgenden Codezeilen in Ihrer Ansichtshierarchie:

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

Zur besseren Verdeutlichung wurde in meinem Blog erklärt: "goaheadwithiphonetech" in Bezug auf "Benutzerdefinierte Beschriftung: Schaltfläche ist kein anklickbares Problem".

Ich hoffe das hilft dir ... !!!

Sandip Patel - SM
quelle
Ihr Blog wurde entfernt. Wo finden wir die Erklärung?
Ishahak