193 votes

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

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 petites choses sur lesquelles cliquer à l'intérieur de cette sous-vue. Nous n'utilisons la sous-vue que pour tirer parti du nouveau cadre (x=0, y=0 dans la sous-vue est en fait 100,100 dans la vue parent).

Ensuite, imaginez que nous avons quelque chose derrière la sous-vue, comme un menu. Je veux que l'utilisateur puisse sélectionner n'importe quelle "petite chose" dans la vue secondaire, mais s'il n'y a rien, je veux que les touches passent à travers elle (puisque l'arrière-plan est clair de toute façon) jusqu'aux boutons situés derrière.

Comment puis-je faire cela ? Il semble que la toucheBegan soit exécutée, mais les boutons ne fonctionnent pas.

1 votes

Je pensais que les UIViews transparents (alpha 0) n'étaient pas censés répondre aux événements tactiles ?

1 votes

J'ai écrit une petite classe juste pour ça. (J'ai ajouté un exemple dans les réponses). La solution est un peu meilleure que la réponse acceptée, car vous pouvez toujours cliquer sur une icône UIButton qui est sous un semi transparent UIView tandis que la partie non transparente de l UIView répondra toujours aux événements tactiles.

331voto

John Stephen Points 2739

Créez une vue personnalisée pour votre conteneur et modifiez le message pointInside : pour qu'il renvoie false lorsque le point ne se trouve pas dans une vue enfant éligible, comme ceci :

Swift :

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 à n'importe lequel de ses enfants de recevoir des touches, mais la vue elle-même sera transparente aux événements.

4 votes

Intéressant. Je dois me plonger davantage dans la chaîne de réponse.

1 votes

Vous devez vérifier si la vue est également visible, alors vous devez également vérifier si les sous-vues sont visibles avant de les tester.

0 votes

J'utilise l'exemple ci-dessus dans mon code de production et il ignore correctement les vues cachées. La méthode pointInside : se charge déjà de vérifier le drapeau caché, d'après ce que je sais.

119voto

akw Points 509

J'utilise également

myView.userInteractionEnabled = NO;

Pas besoin de sous-classe. Cela fonctionne bien.

88 votes

Cela désactivera également l'interaction avec l'utilisateur pour toutes les vues secondaires.

0 votes

Comme l'a mentionné @pixelfreak, 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 ce drapeau reste activé.

20voto

jackslash Points 4719

De Apple :

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

Meilleures pratiques pour les pommes :

N'envoyez pas explicitement les événements vers le haut de la chaîne des répondeurs (via nextResponder) ; invoquez plutôt l'implémentation de la superclasse et laissez UIKit gérer la traversée de la chaîne des répondeurs.

à la place, vous pouvez passer outre :

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

dans votre sous-classe UIView et renvoyer NO si vous voulez que cette touche soit envoyée en haut de la chaîne des répondeurs (c'est-à-dire aux vues situées derrière votre vue sans rien dedans).

1 votes

Ce serait génial d'avoir un lien vers les documents que vous citez, ainsi que les citations elles-mêmes.

1 votes

J'ai ajouté les liens vers les endroits pertinents dans la documentation pour vous

1 votes

~~Pourriez-vous montrer à quoi ressemblerait cette modification ? J'ai trouvé un réponse dans une autre question de ce à quoi ça ressemblerait ; c'est littéralement juste le retour NO ;

6voto

Tod Cunningham Points 702

Dans le prolongement de ce que John a publié, voici un exemple qui permettra aux événements tactiles de passer par 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;
}

0 votes

Il faut vérifier si la vue est visible, et si les sous-vues sont visibles.

0 votes

Une solution parfaite ! Une approche encore plus simple serait de créer un fichier UIView sous-classe PassThroughView qui ne fait que remplacer -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; } si vous voulez transmettre tout événement tactile à des vues inférieures.

1voto

Volure DarkAngel Points 5916

J'ai créé une catégorie à cet effet.

un petit coup de méthode et la vue est dorée.

L'en-tête

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

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

@end

Le dossier de mise en œuvre

#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 aurez besoin d'ajouter mes Swizz.h et Swizz.m

situé Ici

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

chaque vue prendra des points, mais aucun de l'espace vide.

Je recommande également d'utiliser un fond clair.

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

EDIT

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

Fichier d'en-tête

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

Dossier de mise en œuvre

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

0 votes

La méthode passthroughPointInside fonctionne bien pour moi (même sans utiliser aucun des trucs passthroughparent ou swizz - il suffit de renommer passthroughPointInside en pointInside), merci beaucoup.

0 votes

Qu'est-ce que PropertyValueForKey ?

1 votes

Hmm. Personne ne l'a signalé avant. 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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X