web-dev-qa-db-fra.com

Autoriser l'interaction avec un UIView sous un autre UIView

Existe-t-il un moyen simple d'autoriser une interaction avec un bouton dans une vue UIV située sous une autre vue UIView - lorsqu'il n'y a pas d'objets réels de la partie supérieure d'UIView au-dessus du bouton? 

Par exemple, pour le moment, j'ai un UIView (A) avec un objet en haut et un objet en bas de l'écran et rien au milieu. Cela se trouve au-dessus d'un autre UIView qui a des boutons au milieu (B). Cependant, je n'arrive pas à interagir avec les boutons situés au milieu de B.

Je peux voir les boutons dans B - j'ai réglé l'arrière-plan de A sur clearColor - mais les boutons dans B ne semblent pas recevoir de touches, même s'il n'y a aucun objet de A au-dessus de ces boutons.

EDIT- Je souhaite toujours pouvoir interagir avec les objets situés dans la partie supérieure d'UIView

Il existe sûrement un moyen simple de le faire?

114
delany

Vous devez créer une sous-classe UIView pour votre vue de dessus et remplacer la méthode suivante:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // UIView will be "transparent" for touch events if we return NO
    return (point.y < MIDDLE_Y1 || point.y > MIDDLE_Y2);
}

Vous pouvez également consulter la méthode hitTest: event:.

97
gyim

Bien que de nombreuses réponses ici fonctionnent, je suis un peu surpris de voir que la réponse la plus pratique, générique et infaillible n'a pas été donnée ici. @Ash est venu le plus près, sauf qu'il y a quelque chose d'étrange avec le retour de la super vue ... ne faites pas ça.

Cette réponse est tirée d’une réponse que j’ai donnée à une question similaire, ici .

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView == self) return nil;
    return hitView;
}

[super hitTest:point withEvent:event] retournera la vue la plus profonde de la hiérarchie de cette vue qui a été touchée. Si hitView == self (c'est-à-dire s'il n'y a pas de sous-vue sous le point de contact), retournez nil, en spécifiant que cette vue doit non recevoir le toucher. Le fonctionnement de la chaîne de répondeurs signifie que la hiérarchie de vues au-dessus de ce point continuera d'être traversée jusqu'à ce qu'une vue répondant au toucher soit trouvée. Don't renvoie le superview, car ce n'est pas à sa vue si son superview doit accepter les touches ou pas!

Cette solution est:

  • convenient, car il ne nécessite aucune référence à d'autres vues/sous-vues/objets;
  • generic, car il s'applique à toutes les vues qui agissent uniquement en tant que conteneur pour les sous-vues tactiles, et la configuration des sous-vues n'affecte pas son fonctionnement (comme si vous surchargez pointInside:withEvent: pour renvoyer une zone tactile spécifique). .
  • indéréglable, il n'y a pas beaucoup de code ... et le concept n'est pas difficile à comprendre.

J'utilise cela assez souvent que je l'ai résumé dans une sous-classe pour enregistrer des sous-classes de vues sans points pour un remplacement. En prime, ajoutez une propriété pour la rendre configurable:

@interface ISView : UIView
@property(nonatomic, assign) BOOL onlyRespondToTouchesInSubviews;
@end

@implementation ISView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView == self && onlyRespondToTouchesInSubviews) return nil;
    return hitView;
}
@end

Ensuite, déchaînez-vous et utilisez cette vue partout où vous pourriez utiliser un UIView simple. Sa configuration est aussi simple que de définir onlyRespondToTouchesInSubviews à YES.

40
Stuart

Vous pouvez gérer cela de plusieurs manières. Mon préféré est de remplacer hitTest: withEvent: dans une vue qui est un super aperçu commun (peut-être indirectement) aux vues en conflit (on dirait que vous les appelez A et B). Par exemple, quelque chose comme ceci (ici A et B sont des pointeurs UIView, où B est le "caché", normalement ignoré):

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pointInB = [B convertPoint:point fromView:self];

    if ([B pointInside:pointInB withEvent:event])
        return B;

    return [super hitTest:point withEvent:event];
}

Vous pouvez également modifier la méthode pointInside:withEvent: comme suggéré par gyim. Cela vous permet essentiellement d’obtenir le même résultat en "creusant un trou" dans A, au moins pour les retouches.

Une autre approche est la transmission d'événements, ce qui signifie que vous devez remplacer touchesBegan:withEvent: et utiliser des méthodes similaires (telles que touchesMoved:withEvent: etc.) pour envoyer des contacts à un objet différent de celui où ils se sont déplacés. Par exemple, en A, vous pouvez écrire quelque chose comme ceci:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([self shouldForwardTouches:touches]) {
        [B touchesBegan:touches withEvent:event];
    }
    else {
        // Do whatever A does with touches.
    }
}

Cependant, cela ne fonctionnera pas toujours comme vous le souhaitez! L'essentiel est que les contrôles intégrés tels que UIButton ignorent toujours les touches transférées. Pour cette raison, la première approche est plus fiable.

Il y a un bon article de blog expliquant tout cela plus en détail, ainsi qu'un petit projet de travail xcode pour démontrer les idées, disponible ici:

http://bynomial.com/blog/?p=74

30
Tyler

Vous devez définir upperView.userInteractionEnabled = NO;, sinon la vue supérieure interceptera les touches. 

La version de Interface Builder utilisée est une case à cocher au bas du panneau Attributs de la vue appelée "Interaction utilisateur activée". Décochez-la et vous devriez être prêt à partir.

28
Benjamin Cox

L'implémentation personnalisée de pointInside: withEvent: semblait effectivement être la solution, mais traiter avec des coordonnées codées en dur me semblait étrange. J'ai donc fini par vérifier si le CGPoint était à l'intérieur du bouton CGRect à l'aide de la fonction CGRectContainsPoint ():

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    return (CGRectContainsPoint(disclosureButton.frame, point));
}
11
samvermette

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

8
Segev

Riffing juste sur la réponse acceptée et mettre ceci ici pour ma référence. La réponse acceptée fonctionne parfaitement. Vous pouvez l'étendre comme ceci pour permettre aux sous-vues de votre vue de recevoir le contact. OR transmettez-le à toutes les vues derrière nous:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // If one of our subviews wants it, return YES
    for (UIView *subview in self.subviews) {
        CGPoint pointInSubview = [subview convertPoint:point fromView:self];
        if ([subview pointInside:pointInSubview withEvent:event]) {
            return YES;
        }
    }
    // otherwise return NO, as if userInteractionEnabled were NO
    return NO;
}

Remarque: Vous n'avez même pas à faire de récursivité dans l'arborescence de la sous-vue, car chaque méthode pointInside:withEvent: gérera cela pour vous.

4
Dan Rosenstark

Je suppose que je suis un peu en retard pour cette fête, mais je vais ajouter cette solution possible:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView != self) return hitView;
    return [self superview];
}

Si vous utilisez ce code pour remplacer la fonction hitTest standard d'UIView personnalisée, il ignore UNIQUEMENT la vue elle-même. Toutes les sous-vues de cette vue renverront leurs hits normalement, et celles qui auraient été affectées à la vue elle-même sont transmises à sa vue d'ensemble.

-Cendre

4
Ash

La définition de la propriété userInteraction désactivée peut aider. Par exemple:

UIView * topView = [[TOPView alloc] initWithFrame:[self bounds]];
[self addSubview:topView];
[topView setUserInteractionEnabled:NO];

(Remarque: dans le code ci-dessus, «self» fait référence à une vue.)

De cette façon, vous ne pouvez afficher que sur la vue du dessus, mais vous n'obtiendrez pas de données utilisateur Tous les contacts de l'utilisateur traverseront cette vue et la vue inférieure répondra pour eux. Je voudrais utiliser cette topView pour afficher des images transparentes, ou les animer.

3
Aditya

Cette approche est assez propre et permet que les sous-vues transparentes ne réagissent pas aussi bien au toucher. Juste sous-classe UIView et ajoutez la méthode suivante à sa mise en œuvre:

@implementation PassThroughUIView

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    for (UIView *v in self.subviews) {
        CGPoint localPoint = [v convertPoint:point fromView:self];
        if (v.alpha > 0.01 && ![v isHidden] && v.userInteractionEnabled && [v pointInside:localPoint withEvent:event])
            return YES;
    }
    return NO;
}

@end
3
prodos

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
pyRabbit

Vous pouvez faire quelque chose pour intercepter le toucher dans les deux vues.

Vue de dessus:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
   // Do code in the top view
   [bottomView touchesBegan:touches withEvent:event]; // And pass them on to bottomView
   // You have to implement the code for touchesBegan, touchesEnded, touchesCancelled in top/bottom view.
}

Mais c'est l'idée.

2
Alexandre Cassagne

Voici une version de Swift:

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    return !CGRectContainsPoint(buttonView.frame, point)
}
2
Esqarrouth

Ma solution ici:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pointInView = [self.toolkitController.toolbar convertPoint:point fromView:self];

    if ([self.toolkitController.toolbar pointInside:pointInView withEvent:event]) {
       self.userInteractionEnabled = YES;
    } else {
       self.userInteractionEnabled = NO;
    }

    return [super hitTest:point withEvent:event];
}

J'espère que cela t'aides

2
Jerry

Je veux juste poster ceci, parce que j'avais un problème un peu similaire, j'ai passé beaucoup de temps à essayer de mettre en œuvre les réponses sans aucune chance. Ce que j'ai fini par faire:

 for(UIGestureRecognizer *recognizer in topView.gestureRecognizers)
 {
     recognizer.delegate=self;
     [bottomView addGestureRecognizer:recognizer];   
 }
 topView.abView.userInteractionEnabled=NO; 

et implémenter UIGestureRecognizerDelegate:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

La vue de dessous était un contrôleur de navigation avec un nombre de lieues et j'avais une sorte de porte dessus qui pouvait se fermer avec un geste panoramique. Tout était intégré dans un autre VC. Travaillé comme un charme. J'espère que cela t'aides.

1
stringCode

Je pense que la bonne façon consiste à utiliser la chaîne de vues intégrée à la hiérarchie des vues. Pour vos sous-vues poussées dans la vue principale, n'utilisez pas le UIView générique, mais plutôt la sous-classe UIView (ou l'une de ses variantes comme UIImageView) pour créer MYView: UIView (ou le type de supert de votre choix, comme UIImageView) . Dans l'implémentation de YourView, implémentez la méthode touchesBegan. Cette méthode sera ensuite appelée lorsque cette vue est touchée. Tout ce dont vous avez besoin dans cette implémentation est une méthode d'instance:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event ;
{   // cannot handle this event. pass off to super
    [self.superview touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event]; }

cela toucheBegan est une api de répondeur, vous n'avez donc pas besoin de la déclarer dans votre interface publique ou privée; c'est une de ces api magiques que vous devez savoir. Cet auto-aperçu va faire remonter la demande au viewController. Dans le viewController, alors, implémentez ceci pour commencer à manipuler le toucher. 

Notez que l'emplacement des touches (CGPoint) est automatiquement ajusté par rapport à la vue globale à mesure qu'il rebondit dans la chaîne hiérarchique des vues. 

1
PapaSmurf

Je n'ai jamais construit d'interface utilisateur complète à l'aide de la boîte à outils d'interface utilisateur. Je n'ai donc pas beaucoup d'expérience avec celle-ci. Voici ce que je pense devrait fonctionner si.

Chaque UIView, et cette UIWindow, a une propriété subviews, qui est un NSArray contenant toutes les sous-vues.

La première sous-vue que vous ajoutez à une vue recevra l'index 0, et l'index suivant 1, etc. Vous pouvez également remplacer addSubview: par insertSubview: atIndex: ou insertSubview:aboveSubview: et des méthodes permettant de déterminer la position de votre sous-vue dans la hiérarchie.

Vérifiez donc votre code pour voir quelle vue vous ajoutez en premier à votre UIWindow. Ce sera 0, l'autre sera 1.
Maintenant, dans l'une de vos sous-vues, pour atteindre une autre, procédez comme suit:

UIView * theOtherView = [[[self superview] subviews] objectAtIndex: 0];
// or using the properties syntax
UIView * theOtherView = [self.superview.subviews objectAtIndex:0];

Faites-moi savoir si cela fonctionne pour votre cas!


(ci-dessous ce marqueur est ma réponse précédente):

Si les vues doivent communiquer entre elles, elles doivent le faire via un contrôleur (c'est-à-dire en utilisant le modèle MVC populaire).

Lorsque vous créez une nouvelle vue, vous pouvez vous assurer que celle-ci s'enregistre auprès d'un contrôleur.

La technique consiste donc à vous assurer que vos vues s'inscrivent auprès d'un contrôleur (qui peut les stocker par leur nom ou ce que vous préférez dans un dictionnaire ou un tableau). Soit vous pouvez faire en sorte que le contrôleur envoie un message pour vous, soit vous pouvez obtenir une référence à la vue et communiquer avec elle directement.

Si votre vue n'a pas de lien vers le contrôleur (ce qui peut être le cas), vous pouvez utiliser les méthodes singletons et/ou class pour obtenir une référence à votre contrôleur.

1
nash

Mise en œuvre de Swift 4 pour la solution HitTest

let hitView = super.hitTest(point, with: event)
if hitView == self { return nil }
return hitView
1
BiscottiGelato

Dérivée de l'excellente réponse de Stuart, et surtout de la réponse à toute épreuve, et de la mise en œuvre utile de Segev, voici un package Swift 4 que vous pouvez intégrer à tout projet:

extension UIColor {
    static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {

        var pixel: [CUnsignedChar] = [0, 0, 0, 0]

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

        let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)

        context!.translateBy(x: -point.x, y: -point.y)

        view.layer.render(in: context!)

        let red: CGFloat   = CGFloat(pixel[0]) / 255.0
        let green: CGFloat = CGFloat(pixel[1]) / 255.0
        let blue: CGFloat  = CGFloat(pixel[2]) / 255.0
        let alpha: CGFloat = CGFloat(pixel[3]) / 255.0

        let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)

        return color
    }
}

Et puis avec hitTest: 

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
    return super.hitTest(point, with: event)
}
0
brandonscript