Wie kann ich auf eine Schaltfläche hinter einer transparenten UIView klicken?

176

Angenommen, wir haben einen View Controller mit einer Unteransicht. Die Unteransicht nimmt die Mitte des Bildschirms mit 100 px Rändern auf allen Seiten ein. Wir fügen dann ein paar Kleinigkeiten hinzu, auf die wir in dieser Unteransicht klicken können. Wir verwenden die Unteransicht nur, um den neuen Frame zu nutzen (x = 0, y = 0 in der Unteransicht beträgt in der übergeordneten Ansicht tatsächlich 100.100).

Stellen Sie sich dann vor, wir hätten etwas hinter der Unteransicht, wie ein Menü. Ich möchte, dass der Benutzer in der Lage ist, eines der "kleinen Dinge" in der Unteransicht auszuwählen, aber wenn dort nichts ist, möchte ich, dass Berührungen (da der Hintergrund sowieso klar ist) zu den Schaltflächen dahinter gelangen.

Wie kann ich das machen? Es sieht so aus, als würde touchBegan durchlaufen, aber die Tasten funktionieren nicht.

Sean Clark Hess
quelle
1
Ich dachte, transparente (Alpha 0) UIViews sollen nicht auf Berührungsereignisse reagieren?
Evadne Wu
1
Ich habe nur dafür eine kleine Klasse geschrieben. (Ein Beispiel in den Antworten hinzugefügt). Die Lösung dort ist etwas besser als die akzeptierte Antwort, da Sie immer noch auf eine klicken können UIButton, die sich unter einer halbtransparenten befindet, UIViewwährend der nicht transparente Teil der UIViewweiterhin auf Berührungsereignisse reagiert.
Segev

Antworten:

314

Erstellen Sie eine benutzerdefinierte Ansicht für Ihren Container und überschreiben Sie die Nachricht pointInside:, um false zurückzugeben, wenn sich der Punkt nicht in einer berechtigten untergeordneten Ansicht befindet.

Schnell:

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

Ziel 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

Wenn Sie diese Ansicht als Container verwenden, können alle untergeordneten Elemente Berührungen erhalten, die Ansicht selbst ist jedoch für Ereignisse transparent.

John Stephen
quelle
4
Interessant. Ich muss mehr in die Responderkette eintauchen.
Sean Clark Hess
1
Sie müssen überprüfen, ob die Ansicht ebenfalls sichtbar ist, und dann überprüfen, ob die Unteransichten sichtbar sind, bevor Sie sie testen.
The Lazy Coder
2
Leider funktioniert dieser Trick nicht für einen tabBarController, für den die Ansicht nicht geändert werden kann. Hat jemand eine Idee, diese Ansicht auch für Ereignisse transparent zu machen?
Cyrilchampier
1
Sie sollten auch das Alpha der Ansicht überprüfen. Sehr oft verstecke ich eine Ansicht, indem ich das Alpha auf Null setze. Eine Ansicht mit null Alpha sollte sich wie eine versteckte Ansicht verhalten.
James Andrews
2
Ich habe eine schnelle Version gemacht: Vielleicht können Sie sie in die Antwort für Sichtbarkeit aufnehmen gist.github.com/eyeballz/17945454447c7ae766cb
eyeballz
107

Ich benutze auch

myView.userInteractionEnabled = NO;

Keine Notwendigkeit für Unterklassen. Funktioniert gut.

akw
quelle
76
Dies wird auch die Benutzerinteraktion für alle Unteransichten deaktivieren
pixelfreak
Wie @pixelfreak erwähnt, ist dies nicht in allen Fällen die ideale Lösung. In Fällen, in denen die Interaktion mit Unteransichten dieser Ansicht weiterhin erwünscht ist, muss dieses Flag aktiviert bleiben.
Augusto Carmo
20

Von Apple:

Die Ereignisweiterleitung ist eine Technik, die von einigen Anwendungen verwendet wird. Sie leiten Berührungsereignisse weiter, indem Sie die Ereignisbehandlungsmethoden eines anderen Antwortobjekts aufrufen. Obwohl dies eine effektive Technik sein kann, sollten Sie sie mit Vorsicht anwenden. Die Klassen des UIKit-Frameworks sind nicht dafür ausgelegt, Berührungen zu empfangen, die nicht an sie gebunden sind. Wenn Sie Berührungen bedingt an andere Responder in Ihrer Anwendung weiterleiten möchten, sollten alle diese Responder Instanzen Ihrer eigenen Unterklassen von UIView sein.

Best Practice für Äpfel :

Senden Sie keine Ereignisse explizit über die Responderkette (über nextResponder). Rufen Sie stattdessen die Implementierung der Oberklasse auf und lassen Sie das UIKit das Durchlaufen der Responderkette übernehmen.

Stattdessen können Sie Folgendes überschreiben:

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

in Ihrer UIView-Unterklasse und geben Sie NEIN zurück, wenn Sie möchten, dass diese Berührung über die Responderkette gesendet wird (IE für Ansichten hinter Ihrer Ansicht, in denen sich nichts befindet).

Jackslash
quelle
1
Es wäre fantastisch, einen Link zu den Dokumenten zu haben, die Sie zitieren, zusammen mit den Zitaten selbst.
Morgancodes
1
Ich habe die Links zu den relevanten Stellen in der Dokumentation für Sie
hinzugefügt
1
~~ Könnten Sie zeigen, wie diese Überschreibung aussehen müsste? ~~ Eine Antwort in einer anderen Frage gefunden, wie dies aussehen würde; es kehrt buchstäblich gerade zurück NO;
Kontur
Dies sollte wahrscheinlich die akzeptierte Antwort sein. Es ist der einfachste und empfohlene Weg.
Ecuador
8

Eine weitaus einfachere Möglichkeit besteht darin, die im Interface Builder aktivierte Benutzerinteraktion zu deaktivieren. "Wenn Sie ein Storyboard verwenden"

Geben Sie hier die Bildbeschreibung ein

Jeff
quelle
6
Wie andere in einer ähnlichen Antwort betonten, hilft dies in diesem Fall nicht, da es auch Berührungsereignisse in der zugrunde liegenden Ansicht deaktiviert. Mit anderen Worten: Die Schaltfläche unter dieser Ansicht kann nicht getippt werden, unabhängig davon, ob Sie diese Einstellung aktivieren oder deaktivieren.
Auco
6

Aufbauend auf dem, was John gepostet hat, ist hier ein Beispiel, mit dem Berührungsereignisse alle Unteransichten einer Ansicht mit Ausnahme der Schaltflächen durchlaufen können:

-(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
quelle
Sie müssen überprüfen, ob die Ansicht sichtbar ist und ob die Unteransichten sichtbar sind.
The Lazy Coder
Perfekte Lösung! Ein noch einfacherer Ansatz wäre das Erstellen einer UIViewUnterklasse PassThroughView, die nur überschrieben wird, -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }wenn Sie Berührungsereignisse an Unteransichten weiterleiten möchten.
Auco
6

In letzter Zeit habe ich eine Klasse geschrieben, die mir dabei helfen wird. Verwenden Sie es als benutzerdefinierte Klasse für ein UIButtonoderUIView mehrere Berührungsereignisse verwenden, die auf einem transparenten Pixel ausgeführt wurden.

Diese Lösung ist etwas besser als die akzeptierte Antwort, da Sie immer noch auf eine Antwort klicken können UIButton, die sich unter einer halbtransparenten befindet, UIViewwährend der nicht transparente Teil der Lösung UIViewweiterhin auf Berührungsereignisse reagiert.

GIF

Wie Sie im GIF sehen können, ist die Giraffenschaltfläche ein einfaches Rechteck, aber Berührungsereignisse in transparenten Bereichen werden an das UIButtondarunter liegende Gelb weitergeleitet .

Link zur Klasse

Segev
quelle
1
Obwohl diese Lösung möglicherweise funktioniert, ist es besser, die relevanten Teile Ihrer Lösung in die Antwort aufzunehmen.
Koen.
4

Die am besten gewählte Lösung funktionierte für mich nicht vollständig. Ich glaube, das lag daran, dass ich einen TabBarController in der Hierarchie hatte (wie einer der Kommentare hervorhebt), der tatsächlich Berührungen an einige Teile der Benutzeroberfläche weitergab, aber mit meiner durcheinander war Die Fähigkeit von tableView, Berührungsereignisse abzufangen, hat letztendlich hitTest in der Ansicht überschrieben. Ich möchte Berührungen ignorieren und die Unteransichten dieser Ansicht damit umgehen lassen

- (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
quelle
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
quelle
2

Laut dem 'Programmierhandbuch für iPhone-Anwendungen':

Deaktivieren der Zustellung von Berührungsereignissen. Standardmäßig empfängt eine Ansicht Berührungsereignisse. Sie können jedoch die Eigenschaft userInteractionEnabled auf NO setzen, um die Übermittlung von Ereignissen zu deaktivieren. Eine Ansicht empfängt auch keine Ereignisse, wenn sie ausgeblendet oder transparent ist .

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

Aktualisiert : Beispiel entfernt - Frage erneut lesen ...

Haben Sie eine Gestenverarbeitung für die Ansichten, die möglicherweise die Taps verarbeiten, bevor die Schaltfläche sie erhält? Funktioniert die Schaltfläche, wenn Sie keine transparente Ansicht darüber haben?

Irgendwelche Codebeispiele für nicht funktionierenden Code?

LK.
quelle
Ja, ich habe in meiner Frage geschrieben, dass die normalen Berührungen unter Ansichten funktionieren, aber UIButtons und andere Elemente nicht funktionieren.
Sean Clark Hess
1

Soweit ich weiß, sollten Sie dies tun können, indem Sie die Methode hitTest: überschreiben . Ich habe es versucht, konnte es aber nicht richtig zum Laufen bringen.

Am Ende habe ich eine Reihe transparenter Ansichten um das berührbare Objekt erstellt, damit sie es nicht abdecken. Ein bisschen wie ein Hack für mein Problem, das hat gut funktioniert.

Liam
quelle
1

Ausgehend von den anderen Antworten und dem Lesen der Apple-Dokumentation habe ich diese einfache Bibliothek zur Lösung Ihres Problems erstellt:
https://github.com/natrosoft/NATouchThroughView
Es erleichtert das Zeichnen von Ansichten in Interface Builder, an die Berührungen weitergeleitet werden sollen eine zugrunde liegende Ansicht.

Ich denke, Methoden-Swizzling ist übertrieben und im Produktionscode sehr gefährlich, da Sie direkt mit der Basisimplementierung von Apple herumspielen und eine anwendungsweite Änderung vornehmen, die unbeabsichtigte Konsequenzen haben kann.

Es gibt ein Demo-Projekt und hoffentlich macht die README einen guten Job und erklärt, was zu tun ist. Um das OP zu adressieren, würden Sie die übersichtliche UIView, die die Schaltflächen enthält, ändern, um NATouchThroughView in Interface Builder zu klassifizieren. Suchen Sie dann die übersichtliche UIView, die das Menü überlagert, auf das Sie tippen möchten. Ändern Sie diese UIView im Interface Builder in die Klasse NARootTouchThroughView. Es kann sogar die Stamm-UIView Ihres View-Controllers sein, wenn Sie beabsichtigen, diese Berührungen an den zugrunde liegenden View-Controller weiterzuleiten. Schauen Sie sich das Demo-Projekt an, um zu sehen, wie es funktioniert. Es ist wirklich ganz einfach, sicher und nicht invasiv

n8tr
quelle
1

Ich habe dazu eine Kategorie erstellt.

eine kleine Methode swizzling und die Aussicht ist golden.

Der Header

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

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

@end

Die Implementierungsdatei

#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

Sie müssen meine Swizz.h und Swizz.m hinzufügen

befindet sich hier

Danach importieren Sie einfach die Datei UIView + PassthroughParent.h in Ihre Datei {Project} -Prefix.pch, und jede Ansicht verfügt über diese Funktion.

Jede Ansicht nimmt Punkte ein, aber keine der leeren Stellen.

Ich empfehle auch einen klaren Hintergrund.

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

BEARBEITEN

Ich habe meine eigene Eigentumstasche erstellt, die vorher nicht enthalten war.

Header-Datei

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

Implementierungsdatei

// 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
Der faule Codierer
quelle
Die PassthroughPointInside-Methode funktioniert gut für mich (auch ohne die Verwendung von Passthroughparent oder Swizz - benennen Sie PassthroughPointInside einfach in pointInside um), vielen Dank.
Benjamin Piette
Was ist propertyValueForKey?
Skotch
1
Hmm. Niemand hat zuvor darauf hingewiesen. Ich habe ein benutzerdefiniertes Zeigerwörterbuch erstellt, das Eigenschaften in einer Klassenerweiterung enthält. Ich werde sehen, ob ich es hier finden kann.
The Lazy Coder
Swizzling-Methoden sind als heikle Hacking-Lösung für seltene Fälle und Entwickler-Spaß bekannt. Es ist definitiv nicht sicher im App Store, da es sehr wahrscheinlich ist, dass Ihre App beim nächsten Betriebssystem-Update kaputt geht und abstürzt. Warum nicht einfach überschreiben pointInside: withEvent:?
Auco
0

Wenn Sie sich nicht die Mühe machen können, eine Kategorie oder Unterklasse UIView zu verwenden, können Sie die Schaltfläche auch einfach nach vorne bringen, sodass sie sich vor der transparenten Ansicht befindet. Dies ist abhängig von Ihrer Anwendung nicht immer möglich, hat aber bei mir funktioniert. Sie können den Knopf jederzeit wieder zurückbringen oder ausblenden.

Shaked Sayag
quelle
2
Hallo, willkommen zum Stapelüberlauf. Nur ein Hinweis für Sie als neuen Benutzer: Vielleicht möchten Sie auf Ihren Ton achten. Ich würde Sätze wie "wenn Sie sich nicht darum kümmern können" vermeiden. es könnte als herablassend erscheinen
nomistisch
Vielen Dank für das Heads-up. Es war mehr von einem faulen Standpunkt als herablassend, aber Punkt genommen.
Shaked Sayag
0

Versuche dies

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

Versuchen Sie, eine backgroundColorIhrer transparentViewals UIColor(white:0.000, alpha:0.020). Dann können Sie Berührungsereignisse in touchesBegan/ touchesMovedMethoden abrufen. Platzieren Sie den Code unten an einer Stelle, an der Ihre Ansicht angezeigt wird:

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

Ich benutze das, anstatt den Methodenpunkt zu überschreiben (innerhalb von: CGPoint, mit: UIEvent)

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