web-dev-qa-db-fra.com

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

Disons que nous avons un contrôleur de vue avec une vue secondaire. la sous-vue occupe le centre de l'écran avec des marges de 100 pixels de tous les côtés. Nous ajoutons ensuite un tas de petites choses à cliquer dans cette sous-vue. Nous utilisons uniquement la sous-vue pour tirer parti du nouveau cadre (x = 0, y = 0 dans la sous-vue correspond à 100 100 dans la vue parente). 

Ensuite, imaginez que nous ayons quelque chose derrière la sous-vue, comme un menu. Je veux que l'utilisateur puisse sélectionner n'importe lequel des "petits éléments" dans la sous-vue, mais s'il n'y a rien, je souhaite que des touches lui permettent de passer au travers (puisque l'arrière-plan est clair de toute façon) aux boutons situés derrière. 

Comment puis-je faire ceci? Il semblerait que Began soit touché, mais les boutons ne fonctionnent pas. 

161
Sean Clark Hess

Créez une vue personnalisée pour votre conteneur et remplacez le message pointInside: pour renvoyer false lorsque le point ne se trouve 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 en tant que conteneur permettra à n'importe lequel de ses enfants de recevoir des contacts, mais la vue elle-même sera transparente pour les événements.

286
John Stephen

J'utilise aussi 

myView.userInteractionEnabled = NO;

Pas besoin de sous-classe. Fonctionne bien.

88
akw

De Apple:

Le transfert d'événements est une technique utilisée par certaines applications. Vous transmettez des é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 contacts qui ne leur sont pas liés .... Si vous souhaitez transférer conditionnellement les contacts à d'autres répondeurs de votre application, tous ces répondeurs doivent être des instances de vos propres sous-classes d'UIView.

Meilleures pratiques de pommes :

N'envoyez pas explicitement d'événements en haut de la chaîne de répondeurs (via nextResponder); invoquez plutôt l'implémentation de la super-classe et laissez l'UIKit gérer la traversée de la chaîne du répondeur.

à la place, vous pouvez remplacer:

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

dans votre sous-classe UIView et renvoyez NO si vous souhaitez que cette touche soit envoyée dans la chaîne de répondeurs (par exemple, les vues situées derrière votre vue ne contenant rien).

18
jackslash

Une méthode beaucoup plus simple consiste à "désélectionner" l'interaction utilisateur activée dans le générateur d'interface. "si vous utilisez un storyboard"

 enter image description here

7
Jeff

En s'appuyant sur ce que John a publié, voici un exemple permettant 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;
}
6
Tod Cunningham

Dernièrement, j'ai écrit un cours qui m'aidera avec ça. Son utilisation en tant que classe personnalisée pour UIButton ou UIView transmettra les événements tactiles 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 UIButton situé sous un UIView semi transparent tandis que la partie non transparente de UIView répondra toujours aux événements tactiles.

GIF

Comme vous pouvez le constater dans le format GIF, le bouton Girafe est un simple rectangle, mais les événements tactiles situés dans les zones transparentes sont transmis au point jaune UIButton situé en dessous.

Lien vers la classe

6
Segev

La solution votée par Top ne fonctionnait pas vraiment pour moi, je suppose que c’est parce que j’avais un TabBarController dans la hiérarchie (comme l’a souligné un des commentaires). La capacité de tableView d'intercepter les événements tactiles, ce qu'elle a finalement fait était de surcharger hitTest dans la vue, je souhaite 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
    }
}
3
Emilio

Selon le "Guide de programmation d'applications iPhone":

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

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

Updated: Exemple supprimé - relisez la question ...

Avez-vous un traitement de geste sur les vues pouvant traiter les tapotements avant que le bouton ne l'obtienne? Le bouton fonctionne-t-il lorsque vous ne disposez pas de la vue transparente?

Des exemples de code de code qui ne fonctionne pas?

2
LK.

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

une petite méthode qui grésille et la vue est dorée.

L'en-tête

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

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

@end

Le fichier 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 fichier UIView + PassthroughParent.h dans votre fichier {Projet} -Prefix.pch, et chaque vue aura cette possibilité.

chaque vue prendra des points, mais aucun espace vide ne le sera.

Je recommande également d'utiliser un fond clair.

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

MODIFIER 

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
2
The Lazy Coder

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

Autant que je sache, vous êtes censé pouvoir le faire en ignorant la méthode hitTest: . Je l'ai essayé mais je ne pouvais pas le faire fonctionner correctement. 

Finalement, j'ai créé une série de vues transparentes autour de l'objet pouvant être touché afin qu'elles ne le recouvrent pas. Un peu de bidouille pour mon problème cela a bien fonctionné.

1
Liam

En suivant 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
Il est facile de dessiner des vues dans Interface Builder qui doivent transmettre les touches à une vue sous-jacente.

Je pense que le survol de méthode est excessif et très dangereux dans le code de production, car vous bousculez directement l'implémentation de base d'Apple et apportez un changement à l'échelle de l'application pouvant entraîner des conséquences inattendues.

Il y a un projet de démonstration et j'espère que README fait un bon travail en expliquant quoi faire. Pour adresser l'OP, vous devez changer le UIView clair qui contient les boutons en classe NATouchThroughView dans Interface Builder. Recherchez ensuite le UIView clair qui recouvre le menu que vous souhaitez exploiter. Changez cette UIView en classe NARootTouchThroughView dans Interface Builder. Cela peut même être la racine UIView de votre contrôleur de vue si vous souhaitez que ces contacts soient transmis au contrôleur de vue sous-jacent. Consultez le projet de démonstration pour voir comment cela fonctionne. C'est vraiment très simple, sûr et non invasif 

1
n8tr

Si vous ne souhaitez pas utiliser une catégorie ou une sous-classe UIView, vous pouvez également simplement faire avancer le bouton afin qu'il se trouve 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 cacher.

0
Shaked Sayag

Essayez de définir une backgroundColor de votre transparentView comme UIColor(white:0.000, alpha:0.020). Vous pouvez ensuite obtenir des événements tactiles dans les méthodes touchesBegan/touchesMoved. Placez le code ci-dessous quelque part où votre vue est initée:

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

J'utilise cela au lieu de la méthode override (à 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
    }
0
Wei

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
        }
    } 
0
Psajho Nub