Permettre l'interaction avec un UIView sous un autre UIView

115

Existe-t-il un moyen simple d'autoriser l'interaction avec un bouton dans un UIView qui se trouve sous un autre UIView - où il n'y a pas d'objets réels de l'UIView supérieur au-dessus du bouton?

Par exemple, en ce 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 au milieu de B.

Je peux voir les boutons de B - j'ai défini l'arrière-plan de A sur clearColor - mais les boutons de B ne semblent pas recevoir de touches malgré le fait qu'il n'y a pas d'objets de A réellement au-dessus de ces boutons.

EDIT - Je veux toujours pouvoir interagir avec les objets dans le haut UIView

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

Delany
la source
2
C'est à peu près tout expliqué ici: developer.apple.com/iphone/library/documentation/iPhone/... Mais fondamentalement, remplacez hitTest: withEvent :, ils fournissent même un exemple de code.
nash
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:

97

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:.

gyim
la source
19
Que représente middle_y1 / y2 ici?
Jason Renaldo
Je ne sais pas ce que returnfait cette déclaration, mais cela return CGRectContainsPoint(eachSubview.frame, point)fonctionne pour moi. Réponse extrêmement utile sinon
n00neimp0rtant
L'entreprise MIDDLE_Y1 / Y2 n'est qu'un exemple. Cette fonction sera "transparente" pour les événements tactiles dans la MIDDLE_Y1<=y<=MIDDLE_Y2zone.
gyim
41

Bien que la plupart des 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 se passe quelque chose d'étrange avec le retour du superview ... 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]renverra 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 ne doit pas recevoir le toucher. Le fonctionnement de la chaîne de répondeurs signifie que la hiérarchie des vues au-dessus de ce point continuera à être parcourue jusqu'à ce qu'une vue soit trouvée qui répondra au toucher. Ne retournez pas le superview, car ce n'est pas à cette vue si son superview doit accepter les touches ou non!

Cette solution est:

  • pratique , car il ne nécessite aucune référence à d'autres vues / sous-vues / objets;
  • générique , car elle s'applique à toute vue qui agit uniquement comme un conteneur pour les sous-vues tactiles, et la configuration des sous-vues n'affecte pas la façon dont elle fonctionne (comme elle le fait si vous remplacez pointInside:withEvent:pour renvoyer une zone tactile particulière).
  • infaillible , il n'y a pas beaucoup de code ... et le concept n'est pas difficile à comprendre.

Je l'utilise assez souvent pour que je l'ai résumé dans une sous-classe pour enregistrer des sous-classes de vue inutiles 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

Alors devenez sauvage et utilisez cette vue partout où vous pourriez utiliser une plaine UIView. Sa configuration est aussi simple que la mise onlyRespondToTouchesInSubviewsà YES.

Stuart
la source
2
La seule bonne réponse. pour être clair, si vous voulez ignorer les touches sur une vue, mais pas sur les boutons (disons) qui sont contenus dans la vue , faites comme Stuart l'explique. (J'appelle généralement cela une "vue du titulaire" car elle peut "tenir" sans danger certains boutons, mais cela n'affecte rien "en dessous" du support.)
Fattie
Certaines des autres solutions utilisant des points posent des problèmes si votre vue peut faire défiler, comme UITableView et UICollectionView et que vous avez fait défiler vers le haut ou vers le bas. Cette solution fonctionne cependant quel que soit le défilement.
pajevic
31

Il existe plusieurs façons de gérer cela. Mon préféré est de remplacer hitTest: withEvent: dans une vue qui est une vue commune (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 celui "caché", qui est 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 pointInside:withEvent:méthode comme le suggère gyim. Cela vous permet d'obtenir essentiellement le même résultat en «faisant un trou» dans A, au moins pour les touches.

Une autre approche est le transfert d'événement, ce qui signifie touchesBegan:withEvent:des méthodes de substitution et similaires (comme touchesMoved:withEvent:etc.) pour envoyer des touches à un objet différent de celui où elles vont d'abord. 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 comme UIButton ignorent toujours les touchers transférés. 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 xcode fonctionnel pour faire une démonstration des idées, disponible ici:

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

Tyler
la source
Le problème avec cette solution - écrasant hitTest sur la vue de supervision commune - est qu'elle ne permet pas à la vue de dessous de fonctionner entièrement correctement lorsque la vue de dessous fait partie d'un ensemble complet de vues à défilement (MapView, etc.). Sur la vue de dessus, comme suggéré par gyim ci-dessous, fonctionne dans tous les cas pour autant que je sache.
delany
@delany, ce n'est pas vrai; vous pouvez avoir des vues défilantes sous d'autres vues et les laisser fonctionner toutes les deux en remplaçant hitTest. Voici un exemple de code: bynomial.com/blogfiles/Temp32.zip
Tyler
Hey. Pas toutes les vues défilables - juste quelques-unes ... J'ai essayé votre code postal ci-dessus, en changeant UIScrollView en un (par exemple) MKMapView et cela ne fonctionne pas. Les robinets fonctionnent - c'est le défilement qui semble être le problème.
delany
Ok, je l'ai vérifié et confirmé que hitTest ne fonctionne pas comme vous le souhaiteriez avec MKMapView. Vous aviez raison, @delany; bien qu'il fonctionne correctement avec UIScrollView. Je me demande pourquoi MKMapView échoue?
Tyler le
@Tyler Comme vous l'avez mentionné, que "l'essentiel est que les contrôles intégrés comme UIButton ignorent toujours les touchers transférés", je veux savoir comment vous le savez et s'il s'agit d'un document officiel pour expliquer ce comportement. J'ai été confronté à un problème: lorsque UIButton a une sous-vue UIView simple, il ne répond pas aux événements tactiles dans les limites de la sous-vue. Et j'ai trouvé que l'événement est correctement transmis à l'UIButton en tant que comportement par défaut de l'UIView, mais je ne suis pas sûr que ce soit une fonctionnalité désignée ou juste un bogue. Pouvez-vous me montrer la documentation à ce sujet? Merci infiniment.
Neal.Marlin
29

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

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

Benjamin Cox
la source
Désolé - aurait dû dire. Je veux toujours pouvoir interagir avec les objets dans le haut UIView.
delany
Mais le upperView ne peut recevoir aucun contact, incluez le bouton dans le upperView.
imcaptor
2
Cette solution fonctionne pour moi. Je n'avais pas du tout besoin de la vue supérieure pour réagir aux touches.
TJ
11

Implémentation personnalisée de pointInside: withEvent: en effet, cela semblait être la voie à suivre, mais gérer 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 en utilisant la fonction CGRectContainsPoint ():

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

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 est 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
2
Étant donné que votre code n'est pas si long, vous devez inclure les éléments pertinents dans votre réponse, au cas où votre projet serait déplacé ou supprimé à un moment donné dans le futur.
Gavin
Merci, votre solution fonctionne pour mon cas où j'ai besoin de la partie non transparente de mon UIView pour répondre aux événements tactiles, contrairement à la partie transparente. Brillant!
Bruce
@Bruce Heureux que cela vous ait aidé!
Segev
4

Je suppose que je suis un peu en retard à cette soirée, mais j'ajouterai 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'un UIView personnalisé, il ignorera UNIQUEMENT la vue elle-même. Toutes les sous-vues de cette vue renverront leurs hits normalement, et tous les hits qui seraient allés à la vue elle-même sont transmises à sa supervision.

-Cendre

Cendre
la source
1
c'est ma méthode préférée sauf, je ne pense pas que vous devriez revenir [self superview]. la documentation sur cette méthode state "Retourne le descendant le plus éloigné du récepteur dans la hiérarchie de vue (y compris lui-même) qui contient un point spécifié" et "Renvoie nil si le point se trouve complètement en dehors de la hiérarchie de vue du récepteur". je pense que vous devriez revenir nil. lorsque vous retournez nil, le contrôle passera à la supervision pour qu'elle vérifie si elle a des résultats ou non. Donc, fondamentalement, il fera la même chose, sauf que le retour de superview pourrait casser quelque chose dans le futur.
jasongregori
Bien sûr, ce serait probablement sage (notez la date sur ma réponse originale - j'ai beaucoup codé depuis)
Ash
4

Je viens de riffer la réponse acceptée et de la mettre 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 toucher, OU la transmettre à 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 besoin de faire de la récursivité sur l'arborescence des sous-vues, car chaque pointInside:withEvent:méthode gérera cela pour vous.

Dan Rosenstark
la source
3

La désactivation de la propriété userInteraction 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 topView, mais vous n'obtiendrez pas d'entrées utilisateur. Toutes ces touches utilisateur passeront par cette vue et la vue de dessous répondra à leur place. J'utiliserais ce topView pour afficher des images transparentes ou pour les animer.

Aditya
la source
3

Cette approche est assez propre et permet que les sous- vues transparentes ne réagissent pas non plus aux touches. Sous-classez simplement UIViewet ajoutez la méthode suivante à son implémentation:

@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
prodos
la source
2

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

Jerry
la source
2

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.

Alexandre Cassagne
la source
C'est certainement possible - mais c'est beaucoup de travail (vous auriez à rouler vos propres objets tactiles dans la couche inférieure (par exemple des boutons), je pense?) Et il semble étrange que l'on doive rouler les vôtres dans ce façon d'obtenir un comportement qui semble intuitif.
delany
Je ne suis pas sûr que nous devrions simplement essayer.
Alexandre Cassagne
2

Voici une version Swift:

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

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
}
pyRabbit
la source
1

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

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 et ainsi de suite. Vous pouvez également remplacer addSubview:par insertSubview: atIndex:ou insertSubview:aboveSubview:et de telles méthodes qui peuvent 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, à partir de l'une de vos sous-vues, pour en 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!


(sous ce marqueur se trouve 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 qu'elle s'enregistre avec 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 nom ou selon votre préférence dans un dictionnaire ou un tableau). Soit vous pouvez demander au contrôleur d'envoyer un message pour vous, soit vous pouvez obtenir une référence à la vue et communiquer directement avec elle.

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

nash
la source
Merci pour votre réponse - mais je ne suis pas sûr de comprendre. Les deux vues ont un contrôleur - le problème est que, avec une vue au-dessus de l'autre, la vue de dessous ne capte pas les événements (et les transmet à son contrôleur), même si aucun objet ne `` bloque '' réellement ces événements dans la vue de dessus.
delany
Avez-vous une UIWindow en haut de nos UIViews? Si vous le faites, les événements devraient être propagés et vous ne devriez pas avoir besoin de faire de "magie". Lisez à propos de [Window and Views] [1] sur le centre de développement Apple (et bien sûr, ajoutez un autre commentaire si cela ne vous aide pas!) [1]: developer.apple.com/iphone/library/documentation/ iPhone /…
nash
Oui, absolument - une UIWindow en haut de la hiérarchie.
delany
J'ai mis à jour ma réponse pour inclure le code permettant de parcourir une hiérarchie de vues. Faites-moi savoir si vous avez besoin d'une autre aide!
nash
Merci pour votre aide nash - mais je suis assez expérimenté dans la construction de ces interfaces et mon actuelle est correctement configurée pour autant que je sache. Le problème semble être le comportement par défaut des vues complètes lorsqu'elles sont placées au-dessus des autres.
delany
1

Je pense que la bonne façon est d'utiliser la chaîne de vues intégrée à la hiérarchie des vues. Pour vos sous-vues qui sont poussées sur la vue principale, n'utilisez pas l'UIView générique, mais sous-classez UIView (ou l'une de ses variantes comme UIImageView) pour créer MYView: UIView (ou le supertype de votre choix, tel que UIImageView). Dans l'implémentation de YourView, implémentez la méthode touchesBegan. Cette méthode sera alors 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]; }

this touchesBegan est une API de répondeur, vous n'avez donc pas besoin de le déclarer dans votre interface publique ou privée; c'est l'une de ces API magiques que vous devez juste connaître. Ce self.superview fera remonter la requête éventuellement au viewController. Dans viewController, alors, implémentez ce touchBegan pour gérer le toucher.

Notez que l'emplacement des touches (CGPoint) est automatiquement ajusté par rapport à la vue englobante pour vous au fur et à mesure qu'il rebondit dans la chaîne de hiérarchie de vues.

Grand Schtroumpf
la source
1

Je veux juste poster ceci, car j'ai eu un problème quelque peu similaire, j'ai passé beaucoup de temps à essayer de mettre en œuvre des réponses ici 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 la mise en œuvre UIGestureRecognizerDelegate:

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

La vue de dessous était un contrôleur de navigation avec un certain nombre de segues et j'avais une sorte de porte au-dessus qui pouvait se fermer avec un geste panoramique. Le tout était intégré dans un autre VC. A fonctionné comme un charme. J'espère que cela t'aides.

stringCode
la source
1

Implémentation Swift 4 pour une solution basée sur HitTest

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

Dérivé de l'excellente réponse de Stuart, et pour la plupart infaillible, et de l'implémentation utile de Segev, voici un package Swift 4 que vous pouvez ajouter à n'importe quel 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)
}
brandonscript
la source