Comment puis-je cliquer sur un bouton derrière un UIView transparent?

177

Disons que nous avons un contrôleur de vue avec une sous-vue. la sous-vue occupe le centre de l'écran avec des marges de 100 px sur tous les côtés. Nous ajoutons ensuite un tas de petits trucs sur lesquels cliquer dans cette sous-vue. Nous n'utilisons la sous-vue que pour tirer parti du nouveau cadre (x = 0, y = 0 à l'intérieur de la sous-vue est en fait 100,100 dans la vue parente).

Ensuite, imaginez que nous avons quelque chose derrière la sous-vue, comme un menu. Je veux que l'utilisateur puisse sélectionner l'un des "petits trucs" dans la sous-vue, mais s'il n'y a rien là-bas, je veux que les touches le traversent (puisque l'arrière-plan est clair de toute façon) aux boutons derrière.

Comment puis-je faire ceci? On dirait que les touchesBegan passent, mais les boutons ne fonctionnent pas.

Sean Clark Hess
la source
1
Je pensais que les UIViews transparentes (alpha 0) ne sont pas censées répondre aux événements tactiles?
Evadne Wu
1
J'ai écrit une petite classe juste pour ça. (Ajout d'un exemple dans les réponses). La solution est un peu meilleure que la réponse acceptée car vous pouvez toujours cliquer sur un UIButtonqui est sous un semi-transparent UIViewtandis que la partie non transparente du UIViewrépondra toujours aux événements tactiles.
Segev

Réponses:

315

Créez une vue personnalisée pour votre conteneur et remplacez le message pointInside: pour renvoyer false lorsque le point n'est pas dans une vue enfant éligible, comme ceci:

Rapide:

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

Objectif 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

L'utilisation de cette vue comme conteneur permettra à l'un de ses enfants de recevoir des touches, mais la vue elle-même sera transparente aux événements.

John Stephen
la source
4
Intéressant. J'ai besoin de plonger davantage dans la chaîne des répondeurs.
Sean Clark Hess
1
vous devez vérifier si la vue est également visible, puis vous devez également vérifier si les sous-vues sont visibles avant de les tester.
The Lazy Coder
2
malheureusement cette astuce ne fonctionne pas pour un tabBarController, pour lequel la vue ne peut pas être modifiée. Quelqu'un a-t-il une idée pour rendre cette vue transparente aux événements aussi?
cyrilchampier
1
Vous devez également vérifier l'alpha de la vue. Très souvent, je cache une vue en mettant l'alpha à zéro. Une vue avec zéro alpha doit agir comme une vue masquée.
James Andrews
2
J'ai fait une version rapide: peut-être que vous pouvez l'inclure dans la réponse pour la visibilité gist.github.com/eyeballz/17945454447c7ae766cb
eyeballz
107

J'utilise aussi

myView.userInteractionEnabled = NO;

Pas besoin de sous-classe. Fonctionne très bien.

akw
la source
76
Cela désactivera également l'interaction de l'utilisateur pour toutes les sous
vues
Comme @pixelfreak l'a mentionné, ce n'est pas la solution idéale pour tous les cas. Les cas où l'interaction sur les sous-vues de cette vue est toujours souhaitée nécessitent que l'indicateur reste activé.
Augusto Carmo
20

De Apple:

Le transfert d'événement est une technique utilisée par certaines applications. Vous transférez les événements tactiles en appelant les méthodes de gestion des événements d'un autre objet répondeur. Bien que cela puisse être une technique efficace, vous devez l'utiliser avec prudence. Les classes du framework UIKit ne sont pas conçues pour recevoir des touches qui ne sont pas liées à elles .... Si vous souhaitez transférer conditionnellement des touches à d'autres répondeurs dans votre application, tous ces répondeurs doivent être des instances de vos propres sous-classes d'UIView.

Meilleures pratiques pour les pommes :

N'envoyez pas explicitement d'événements dans la chaîne de répondeurs (via nextResponder); à la place, appelez l'implémentation de la superclasse et laissez l'UIKit gérer le parcours de la chaîne de répondeurs.

à la place, vous pouvez remplacer:

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

dans votre sous-classe UIView et renvoyez NON si vous voulez que ce contact soit envoyé dans la chaîne de répondeurs (c'est-à-dire aux vues derrière votre vue sans rien dedans).

jackslash
la source
1
Ce serait génial d'avoir un lien vers les documents que vous citez, ainsi que les citations elles-mêmes.
morgancodes
1
J'ai ajouté les liens vers les endroits pertinents dans la documentation pour vous
jackslash
1
~~ Pourriez-vous montrer à quoi devrait ressembler ce remplacement? ~~ Vous avez trouvé une réponse dans une autre question sur ce à quoi cela ressemblerait; il revient littéralement NO;
kontur le
Cela devrait probablement être la réponse acceptée. C'est le moyen le plus simple et le moyen recommandé.
Équateur
8

Un moyen beaucoup plus simple consiste à "Décocher" l'interaction utilisateur activée dans le générateur d'interface. "Si vous utilisez un storyboard"

entrez la description de l'image ici

Jeff
la source
6
comme d'autres l'ont souligné dans une réponse similaire, cela n'aide pas dans ce cas, car cela désactive également les événements tactiles dans la vue sous-jacente. En d'autres termes: le bouton sous cette vue ne peut pas être touché, que vous activiez ou désactiviez ce paramètre.
auco
6

Sur la base de ce que John a publié, voici un exemple qui permettra aux événements tactiles de passer à travers toutes les sous-vues d'une vue, à l'exception des boutons:

-(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
la source
Besoin de vérifier si la vue est visible et si les sous-vues sont visibles.
The Lazy Coder
Solution parfaite! Une approche encore plus simple consisterait à créer une UIViewsous PassThroughView- classe qui remplace simplement -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }si vous souhaitez transférer des événements tactiles vers les vues inférieures.
auco
6

Dernièrement, j'ai écrit un cours qui m'aidera avec cela. L'utiliser comme classe personnalisée pour un UIButtonou UIViewpassera les événements tactiles qui ont été exécutés sur un pixel transparent.

Cette solution est un peu meilleure que la réponse acceptée car vous pouvez toujours cliquer sur un UIButtonqui se trouve sous un semi-transparent UIViewtandis que la partie non transparente du UIViewrépondra toujours aux événements tactiles.

GIF

Comme vous pouvez le voir dans le GIF, le bouton Girafe est un simple rectangle mais les événements tactiles sur les zones transparentes sont transmis au jaune en UIButtondessous.

Lien vers la classe

Segev
la source
1
Bien que cette solution puisse fonctionner, il est préférable de placer les parties pertinentes de votre solution dans la réponse.
Koen.
4

La solution la plus votée ne fonctionnait pas pleinement pour moi, je suppose que c'était parce que j'avais un TabBarController dans la hiérarchie (comme le souligne l'un des commentaires), elle transmettait en fait des touches à certaines parties de l'interface utilisateur, mais cela dérangeait mon La capacité de tableView à intercepter les événements tactiles, ce qui a finalement été de remplacer hitTest dans la vue Je veux ignorer les touches et laisser les sous-vues de cette vue les gérer

- (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
la source
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
la source
2

Selon le 'Guide de programmation des applications iPhone':

Désactivation de la diffusion des événements tactiles. Par défaut, une vue reçoit des événements tactiles, mais vous pouvez définir sa propriété userInteractionEnabled sur NO pour désactiver la livraison des événements. Une vue ne reçoit pas non plus d'événements si elle est masquée ou si elle est transparente .

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

Mise à jour : exemple supprimé - relire la question ...

Avez-vous un traitement des gestes sur les vues qui peuvent traiter les tapotements avant que le bouton ne l'obtienne? Le bouton fonctionne-t-il lorsque vous n'avez pas de vue transparente dessus?

Des échantillons de code de code non fonctionnel?

LK.
la source
Oui, j'ai écrit dans ma question que les touches normales fonctionnent dans les vues inférieures, mais les UIButtons et autres éléments ne fonctionnent pas.
Sean Clark Hess
1

Autant que je sache, vous êtes censé pouvoir le faire en remplaçant la méthode hitTest:. Je l'ai essayé mais je n'ai pas pu le faire fonctionner correctement.

À la fin, j'ai créé une série de vues transparentes autour de l'objet palpable pour qu'elles ne le recouvrent pas. Un peu de hack pour mon problème, cela a bien fonctionné.

Liam
la source
1

En prenant les conseils des autres réponses et en lisant la documentation d'Apple, j'ai créé cette bibliothèque simple pour résoudre votre problème:
https://github.com/natrosoft/NATouchThroughView
Cela permet de dessiner facilement des vues dans Interface Builder qui devraient passer par une vue sous-jacente.

Je pense que le swizzling de méthode est excessif et très dangereux à faire dans le code de production, car vous dérangez directement l'implémentation de base d'Apple et apportez un changement à l'échelle de l'application qui pourrait avoir des conséquences inattendues.

Il y a un projet de démonstration et j'espère que le README fait un bon travail en expliquant ce qu'il faut faire. Pour adresser l'OP, vous devez modifier l'UIView clair qui contient les boutons en classe NATouchThroughView dans Interface Builder. Recherchez ensuite la vue UIView claire qui recouvre le menu que vous souhaitez activer. Remplacez cet UIView par la classe NARootTouchThroughView dans Interface Builder. Il peut même s'agir de l'UIView racine de votre contrôleur de vue si vous souhaitez que ces touches passent au contrôleur de vue sous-jacent. Consultez le projet de démonstration pour voir comment cela fonctionne. C'est vraiment assez simple, sûr et non invasif

n8tr
la source
1

J'ai créé une catégorie pour ce faire.

une petite méthode swizzling et la vue est d'or.

L'en-tête

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

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

@end

Le dossier d'implémentation

#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

Vous devrez ajouter mon Swizz.h et Swizz.m

situé ici

Après cela, il vous suffit d'importer le UIView + PassthroughParent.h dans votre fichier {Project} -Prefix.pch, et chaque vue aura cette capacité.

chaque vue prendra des points, mais aucun des espaces vides ne le fera.

Je recommande également d'utiliser un arrière-plan clair.

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

ÉDITER

J'ai créé mon propre sac de propriété, et cela n'était pas inclus auparavant.

En tête de fichier

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

Fichier d'implémentation

// 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
Le codeur paresseux
la source
La méthode passthroughPointInside fonctionne bien pour moi (même sans utiliser aucun des trucs passthroughparent ou swizz - renommez simplement passthroughPointInside en pointInside), merci beaucoup.
Benjamin Piette
Qu'est-ce que propertyValueForKey?
Skotch
1
Hmm. Personne ne l'a signalé auparavant. J'ai construit un dictionnaire de pointeurs personnalisé qui contient des propriétés dans une extension de classe. Je vais voir si je peux le trouver pour l'inclure ici.
The Lazy Coder
Les méthodes Swizzling sont connues pour être une solution de piratage délicate pour les cas rares et le plaisir des développeurs. Ce n'est certainement pas sûr l'App Store, car il risque fort de casser et de planter votre application avec la prochaine mise à jour du système d'exploitation. Pourquoi ne pas simplement passer outre pointInside: withEvent:?
auco
0

Si vous ne pouvez pas prendre la peine d'utiliser une catégorie ou une sous-classe UIView, vous pouvez également simplement faire avancer le bouton pour qu'il soit devant la vue transparente. Cela ne sera pas toujours possible en fonction de votre application, mais cela a fonctionné pour moi. Vous pouvez toujours ramener le bouton ou le masquer.

Shaked Sayag
la source
2
Bonjour, bienvenue dans le débordement de pile. Juste un indice pour vous en tant que nouvel utilisateur: vous voudrez peut-être faire attention à votre ton. J'éviterais des phrases comme "si vous ne pouvez pas vous embêter"; cela peut paraître condescendant
nomiste
Merci pour la mise en garde. C'était plus d'un point de vue paresseux que condescendant, mais point pris.
Shaked Sayag
0

Essaye ça

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
la source
0

Essayez de définir un backgroundColorde vos transparentViewcomme UIColor(white:0.000, alpha:0.020). Ensuite, vous pouvez obtenir des événements tactiles dans touchesBegan/ touchesMovedmethods. Placez le code ci-dessous à un endroit où votre vue est ouverte:

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

J'utilise cela au lieu du point de méthode de remplacement (à l'intérieur: CGPoint, avec: UIEvent)

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