Jak mogę kliknąć przycisk za przezroczystym UIView?

177

Powiedzmy, że mamy kontroler widoku z jednym widokiem podrzędnym. Widok podrzędny zajmuje środek ekranu z marginesami 100 pikseli ze wszystkich stron. Następnie dodajemy kilka drobiazgów do kliknięcia w tym podwidoku. Używamy tylko widoku podrzędnego, aby skorzystać z nowej klatki (x = 0, y = 0 wewnątrz widoku podrzędnego to w rzeczywistości 100,100 w widoku nadrzędnym).

Następnie wyobraź sobie, że mamy coś w podglądzie, na przykład menu. Chcę, aby użytkownik mógł wybrać dowolne „małe rzeczy” w podglądzie, ale jeśli nic tam nie ma, chcę, aby dotknięcia przechodziły przez to (ponieważ tło i tak jest czyste) do przycisków za nim.

W jaki sposób mogę to zrobić? Wygląda na to, że przeszedł przez dotknięcie, ale przyciski nie działają.

Sean Clark Hess
źródło
1
Myślałem, że przezroczyste (alfa 0) UIViews nie powinny odpowiadać na zdarzenia dotykowe?
Evadne Wu
1
Właśnie w tym celu napisałem małą klasę. (Dodano przykład w odpowiedziach). Rozwiązanie jest nieco lepsze niż zaakceptowana odpowiedź, ponieważ nadal można kliknąć, UIButtonktóra jest pod półprzezroczystą, UIViewpodczas gdy nieprzezroczysta część UIViewbędzie nadal reagować na zdarzenia dotykowe.
Segev

Odpowiedzi:

315

Utwórz niestandardowy widok dla swojego kontenera i nadpisz pointInside: komunikat, aby zwracał fałsz, gdy punkt nie znajduje się w odpowiednim widoku podrzędnym, na przykład:

Szybki:

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

Cel 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

Używanie tego widoku jako kontenera pozwoli każdemu z jego dzieci na otrzymanie dotknięć, ale sam widok będzie przezroczysty dla wydarzeń.

John Stephen
źródło
4
Ciekawy. Muszę bardziej zagłębić się w łańcuch odpowiedzi.
Sean Clark Hess
1
musisz sprawdzić, czy widok jest również widoczny, a następnie musisz sprawdzić, czy podwidoki są widoczne przed ich przetestowaniem.
The Lazy Coder
2
niestety ta sztuczka nie działa dla tabBarController, dla którego nie można zmienić widoku. Czy ktoś ma pomysł, aby ten widok był przejrzysty również dla wydarzeń?
cyrilchampier
1
Powinieneś także sprawdzić alfa widoku. Dość często ukrywam widok, ustawiając alfa na zero. Widok z zerową wartością alfa powinien działać jak widok ukryty.
James Andrews,
2
Zrobiłem szybką wersję: może możesz dołączyć ją do odpowiedzi na temat widoczności gist.github.com/eyeballz/17945454447c7ae766cb
eyeballz
107

Ja też używam

myView.userInteractionEnabled = NO;

Nie ma potrzeby podklasy. Działa w porządku.

akw
źródło
76
Spowoduje to również wyłączenie interakcji użytkownika z jakimikolwiek podglądami
pixelfreak
Jak wspomniał @pixelfreak, nie jest to idealne rozwiązanie dla wszystkich przypadków. Przypadki, w których interakcja na widokach podrzędnych tego widoku jest nadal pożądana, wymagają, aby flaga pozostała włączona.
Augusto Carmo
20

Od Apple:

Przekazywanie zdarzeń to technika używana w niektórych aplikacjach. Zdarzenia dotykowe przekazujesz dalej, wywołując metody obsługi zdarzeń innego obiektu odpowiadającego. Chociaż może to być skuteczna technika, należy jej używać ostrożnie. Klasy frameworka UIKit nie są przeznaczone do otrzymywania dotknięć, które nie są z nimi związane .... Jeśli chcesz warunkowo przekazywać dotknięcia innym respondentom w Twojej aplikacji, wszystkie te odpowiadające powinny być instancjami Twoich własnych podklas UIView.

Najlepsze praktyki dotyczące jabłek :

Nie wysyłaj jawnie zdarzeń w łańcuchu odpowiedzi (przez nextResponder); zamiast tego wywołaj implementację superklasy i pozwól, aby UIKit zajął się przemierzaniem łańcucha odpowiedzi.

zamiast tego możesz zastąpić:

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

w podklasie UIView i zwróć NIE, jeśli chcesz, aby to dotknięcie zostało przesłane w górę łańcucha responderów (tj. do widoków za twoim widokiem bez niczego).

jackslash
źródło
1
Byłoby wspaniale mieć link do cytowanych dokumentów wraz z samymi cytatami.
Morgancodes
1
Dodałem linki do odpowiednich miejsc w dokumentacji dla Ciebie
jackslash
1
~~ Czy mógłbyś pokazać, jak musiało to wyglądać? ~~ Znalazłem odpowiedź w innym pytaniu, jak to by wyglądało; dosłownie po prostu powraca NO;
kontur
To prawdopodobnie powinna być akceptowana odpowiedź. To najprostszy sposób i zalecany sposób.
Ekwador
8

O wiele prostszym sposobem jest „odznaczenie” włączonej interakcji użytkownika w kreatorze interfejsu. „Jeśli używasz scenorysu”

wprowadź opis obrazu tutaj

Jeff
źródło
6
jak wskazywali inni w podobnej odpowiedzi, nie pomaga to w tym przypadku, ponieważ wyłącza również zdarzenia dotykowe w podstawowym widoku. Innymi słowy: przycisk poniżej tego widoku nie może zostać dotknięty, niezależnie od tego, czy włączysz czy wyłączysz to ustawienie.
auco
6

Opierając się na tym, co opublikował John, oto przykład, który pozwoli zdarzeniom dotykowym na przechodzenie przez wszystkie podglądy widoku z wyjątkiem przycisków:

-(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
źródło
Trzeba sprawdzić, czy widok jest widoczny i czy widoki podrzędne są widoczne.
The Lazy Coder,
Idealne rozwiązanie! Jeszcze prostszym podejściem byłoby utworzenie UIViewpodklasy, PassThroughViewktóra po prostu nadpisuje, -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }jeśli chcesz przekazać wszystkie zdarzenia dotyku do poniższych widoków.
auco
6

Ostatnio napisałem zajęcia, które pomogą mi właśnie w tym. Użycie jej jako klasy niestandardowej dla UIButtonlub UIViewspowoduje przekazanie zdarzeń dotykowych, które zostały wykonane na przezroczystym pikselu.

To rozwiązanie jest nieco lepsze niż zaakceptowana odpowiedź, ponieważ nadal możesz kliknąć element UIButtonznajdujący się pod półprzezroczystą, UIViewpodczas gdy nieprzezroczysta część UIViewnadal będzie reagować na zdarzenia dotykowe.

GIF

Jak widać na GIF-ie, przycisk Żyrafa jest prostym prostokątem, ale zdarzenia dotykowe na przezroczystych obszarach są przenoszone na żółty UIButtonpod spodem.

Link do zajęć

Segev
źródło
1
Chociaż to rozwiązanie może działać, lepiej umieścić w odpowiedzi odpowiednie części rozwiązania.
Koen.
4

Najlepiej ocenione rozwiązanie nie działało w pełni dla mnie, myślę, że to dlatego, że miałem TabBarController w hierarchii (jak wskazuje jeden z komentarzy), w rzeczywistości przekazywało poprawki do niektórych części interfejsu użytkownika, ale mieszało Zdolność tableView do przechwytywania zdarzeń dotykowych, ostatecznie nadpisuje hitTest w widoku Chcę zignorować dotknięcia i pozwolić, aby podwidoki tego widoku obsłużyły je

- (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
źródło
3

Szybki 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
źródło
2

Zgodnie z „Przewodnikiem programowania aplikacji na iPhone'a”:

Wyłączanie dostarczania zdarzeń dotykowych. Domyślnie widok odbiera zdarzenia dotyku, ale można ustawić jego właściwość userInteractionEnabled na NO, aby wyłączyć dostarczanie zdarzeń. Widok również nie odbiera zdarzeń, jeśli jest ukryty lub przezroczysty .

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

Zaktualizowano : Usunięto przykład - ponownie przeczytaj pytanie ...

Czy masz jakieś przetwarzanie gestów na widokach, które mogą przetwarzać stuknięcia, zanim przycisk je otrzyma? Czy przycisk działa, gdy nie masz nad nim przezroczystego widoku?

Jakieś próbki kodu niedziałającego kodu?

LK.
źródło
Tak, napisałem w swoim pytaniu, że normalne dotknięcia działają pod widokami, ale UIButtons i inne elementy nie działają.
Sean Clark Hess
1

O ile wiem, powinieneś być w stanie to zrobić, zastępując metodę hitTest:. Próbowałem, ale nie mogłem sprawić, by działał poprawnie.

Na koniec stworzyłem serię przezroczystych widoków wokół dotykalnego obiektu, tak aby go nie zakrywały. Trochę hack do mojego problemu, to działało dobrze.

Liam
źródło
1

Biorąc wskazówki z innych odpowiedzi i czytając dokumentację Apple, stworzyłem tę prostą bibliotekę, aby rozwiązać Twój problem:
https://github.com/natrosoft/NATouchThroughView
Ułatwia to rysowanie widoków w Interface Builder, które powinny przejść do podstawowy pogląd.

Myślę, że zawijanie metod jest przesadą i bardzo niebezpieczne w kodzie produkcyjnym, ponieważ bezpośrednio mieszasz w podstawową implementację Apple i wprowadzasz zmianę w całej aplikacji, która może spowodować niezamierzone konsekwencje.

Jest projekt demonstracyjny i miejmy nadzieję, że README dobrze się spisuje, wyjaśniając, co należy zrobić. Aby rozwiązać problem, należy zmienić przejrzysty UIView, który zawiera przyciski, na klasę NATouchThroughView w programie Interface Builder. Następnie znajdź przejrzysty UIView, który nakłada się na menu, które chcesz, aby można było kliknąć. Zmień to UIView na klasę NARootTouchThroughView w programie Interface Builder. Może to być nawet główny UIView kontrolera widoku, jeśli zamierzasz te dotknięcia przejść do podstawowego kontrolera widoku. Sprawdź projekt demonstracyjny, aby zobaczyć, jak to działa. To naprawdę całkiem proste, bezpieczne i nieinwazyjne

n8tr
źródło
1

Stworzyłem kategorię, aby to zrobić.

trochę zawirowania metody i widok jest złoty.

Nagłówek

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

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

@end

Plik implementacji

#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

Będziesz musiał dodać mój Swizz.h i Swizz.m

znajduje się tutaj

Następnie wystarczy zaimportować plik UIView + PassthroughParent.h do pliku {Project} -Prefix.pch, a każdy widok będzie miał taką możliwość.

każdy widok przyjmie punkty, ale żadne puste miejsce nie.

Polecam również użycie przezroczystego tła.

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

EDYTOWAĆ

Stworzyłem własną torbę z majątkiem, która wcześniej nie była dołączana.

Plik nagłówkowy

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

Plik implementacyjny

// 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
Leniwy koder
źródło
passthroughPointInside działa dobrze dla mnie (nawet bez używania jakichkolwiek elementów przekazujących lub swizz - po prostu zmień nazwę passthroughPointInside na pointInside), wielkie dzięki.
Benjamin Piette
Co to jest propertyValueForKey?
Skotch
1
Hmm. Nikt wcześniej tego nie zauważył. Zbudowałem niestandardowy słownik wskaźników, który przechowuje właściwości w rozszerzeniu klasy. Zobaczę, czy uda mi się go znaleźć i zamieścić tutaj.
The Lazy Coder
Wiadomo, że metody Swizzling są delikatnym rozwiązaniem hakerskim w rzadkich przypadkach i zabawie programistów. Zdecydowanie nie jest to bezpieczne w App Store, ponieważ jest bardzo prawdopodobne, że zepsuje i zawiesi Twoją aplikację przy następnej aktualizacji systemu operacyjnego. Dlaczego po prostu nie zastąpić pointInside: withEvent:?
auco
0

Jeśli nie możesz zawracać sobie głowy użyciem kategorii lub podklasy UIView, możesz po prostu przesunąć przycisk do przodu, tak aby znajdował się przed przezroczystym widokiem. W zależności od aplikacji nie zawsze będzie to możliwe, ale dla mnie zadziałało. Zawsze możesz przywrócić przycisk z powrotem lub go ukryć.

Shaked Sayag
źródło
2
Cześć, witamy w przepełnieniu stosu. Tylko wskazówka dla nowego użytkownika: warto uważać na swój ton. Unikałbym zwrotów takich jak „jeśli nie możesz się przejmować”; może się to wydawać protekcjonalne
nomistyczne
Dzięki za ostrzeżenia… To było bardziej z leniwego punktu widzenia niż protekcjonalności, ale uwaga.
Shaked Sayag
0

Spróbuj tego

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
źródło
0

Spróbuj ustawić backgroundColortwoich transparentViewjak UIColor(white:0.000, alpha:0.020). Następnie możesz uzyskać zdarzenia dotykowe w touchesBegan/ touchesMovedMethods. Umieść poniższy kod w miejscu, w którym jest uruchamiany widok:

self.alpha = 1
self.backgroundColor = UIColor(white: 0.0, alpha: 0.02)
self.isMultipleTouchEnabled = true
self.isUserInteractionEnabled = true
Agisight
źródło
0

Używam tego zamiast nadpisywania punktu metody (wewnątrz: CGPoint, z: UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }
Wei
źródło