How can I click a button behind a transparent UIView?
Asked Answered
K

16

203

Let's say we have a view controller with one sub view. the subview takes up the center of the screen with 100 px margins on all sides. We then add a bunch of little stuff to click on inside that subview. We are only using the subview to take advantage of the new frame ( x=0, y=0 inside the subview is actually 100,100 in the parent view).

Then, imagine that we have something behind the subview, like a menu. I want the user to be able to select any of the "little stuff" in the subview, but if there is nothing there, I want touches to pass through it (since the background is clear anyway) to the buttons behind it.

How can I do this? It looks like touchesBegan goes through, but buttons don't work.

Kalif answered 15/6, 2010 at 15:51 Comment(3)
I thought transparent (alpha 0) UIViews aren’t supposed to respond to touch events?Theme
I've written a small class just for that. (Added an example in the answers). The solution there a somewhat better than the accepted answer because you can still click a UIButton that is under a semi transparent UIView while the non transparent part of the UIView will still respond to touch events.Scriptorium
Beware: Since iOS 14, UIStackView is a rendering view. That means it can have a background. And even if it's .clear color, it won't pass touch events to underlying views.Understudy
C
341

Create a custom view for your container and override the pointInside: message to return false when the point isn't within an eligible child view, like this:

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

Objective 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

Using this view as a container will allow any of its children to receive touches but the view itself will be transparent to events.

Claraclarabella answered 24/10, 2010 at 22:30 Comment(11)
Interesting. I need to dive into the responder chain more.Kalif
you need to check if the view is visible as well, then you also need to check if the subviews are visible before testing them.Decrial
I use the above example in my production code and it correctly ignores hidden views. The pointInside: method already takes care of checking the hidden flag as far as I can tell.Claraclarabella
Ignore my above comment, I should have looked at my production code before writing that. You are correct that it needs to have that check -- I'm updating the example to include the checks I'm using in my own code.Claraclarabella
unfortunately this trick does not work for a tabBarController, for which the view cannot be changed. Anybody has an idea to make this view transparent to events too ?Rotarian
Works great! I've been trying to figure out a solution like this for a while. Setting userInteractionEnabled just doesn't work well in some cases.Pentad
You should also check the alpha of the view. Quite often I'll hide a view by setting the alpha to zero. A view with zero alpha should act like a hidden view.Blowout
I did a swift Version: maybe you can include it in the answer for visibility gist.github.com/eyeballz/17945454447c7ae766cbPervasive
this answer really help me, note: you need to set PassThroughView in both Container and Embed ViewConstitutionally
@Rotarian had the same problem and found a solution that works for me, check my answer down below, basically consist in overriding hitTestBiophysics
You should also implement - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {Blister
D
127

I also use

myView.userInteractionEnabled = NO;

No need to subclass. Works fine.

Developer answered 1/9, 2011 at 20:7 Comment(2)
This will also disable user interaction for any subviewsAlbum
As @Album mentioned, this is not the ideal solution for all cases. Cases where the interaction on subviews of that view are still desired require that flag to remain enabled.Semivitreous
V
20

From Apple:

Event forwarding is a technique used by some applications. You forward touch events by invoking the event-handling methods of another responder object. Although this can be an effective technique, you should use it with caution. The classes of the UIKit framework are not designed to receive touches that are not bound to them .... If you want to conditionally forward touches to other responders in your application, all of these responders should be instances of your own subclasses of UIView.

Apples Best Practise:

Do not explicitly send events up the responder chain (via nextResponder); instead, invoke the superclass implementation and let the UIKit handle responder-chain traversal.

instead you can override:

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

in your UIView subclass and return NO if you want that touch to be sent up the responder chain (I.E. to views behind your view with nothing in it).

Velocipede answered 28/10, 2011 at 0:28 Comment(4)
It would be awesome to have a link to the docs you're quoting, along with the quotes themselves.Dupery
I added the links to the relevant places in the documentation for youVelocipede
~~Could you show how that override would have to look like?~~ Found a answer in another question of how this would look like; it's literally just returning NO;Anterior
This should probably be the accepted answer. It's the simplest way and the recommended way.Jakejakes
P
9

A far simpler way is to "Un-Check" User Interaction Enabled in the interface builder. "If you are using a storyboard"

enter image description here

Polk answered 31/1, 2016 at 20:17 Comment(1)
as others pointed out in a similar answer, this does not help in this case, as it's also disabling touch events in the underlying view. In other words: the button below that view cannot be tapped, regardless if you en- or disable this setting.Viddah
B
7

Top voted solution was not fully working for me, I guess it was because I had a TabBarController into the hierarchy (as one of the comments points out) it was in fact passing along touches to some parts of the UI but it was messing with my tableView's ability to intercept touch events, what finally did it was overriding hitTest in the view I want to ignore touches and let the subviews of that view handle them

- (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
    }
}
Biophysics answered 12/2, 2018 at 11:32 Comment(1)
This should be the answer. Super clean.Mystagogue
C
6

Building on what John posted, here is an example that will allow touch events to pass through all subviews of a view except for buttons:

-(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;
}
Corriecorriedale answered 2/2, 2012 at 5:25 Comment(2)
Need to check if the view is visible, and if the subviews are visible.Decrial
Perfect solution! An even simpler approach would be to create a UIView subclass PassThroughView that just overrides -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; } if you want to forward any touch events to underneath views.Viddah
S
6

Lately I wrote a class that will help me with just that. Using it as a custom class for a UIButton or UIView will pass touch events that were executed on a transparent pixel.

This solution is a somewhat better than the accepted answer because you can still click a UIButton that is under a semi transparent UIView while the non transparent part of the UIView will still respond to touch events.

GIF

As you can see in the GIF, the Giraffe button is a simple rectangle but touch events on transparent areas are passed on to the yellow UIButton underneath.

Link to class

Scriptorium answered 1/4, 2015 at 8:31 Comment(1)
While this solution may work, it's better to place the relevant parts of your solution within the answer.Contentment
L
4

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
}
Loutish answered 16/1, 2018 at 8:33 Comment(0)
S
2

According to the 'iPhone Application Programming Guide':

Turning off delivery of touch events. By default, a view receives touch events, but you can set its userInteractionEnabled property to NO to turn off delivery of events. A view also does not receive events if it’s hidden or if it’s transparent.

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

Updated: Removed example - reread the question...

Do you have any gesture processing on the views that may be processing the taps before the button gets it? Does the button work when you don't have the transparent view over it?

Any code samples of non-working code?

Singlehandedly answered 16/6, 2010 at 4:59 Comment(1)
Yes, I wrote in my question that the normal touches work in underneath views, but UIButtons and other elements don't work.Kalif
R
1

As far as I know, you are supposed to be able to do this by overriding the hitTest: method. I did try it but could not get it to work properly.

In the end I created a series of transparent views around the touchable object so that they did not cover it. Bit of a hack for my issue this worked fine.

Reiterate answered 15/6, 2010 at 16:10 Comment(0)
D
1

I created a category to do this.

a little method swizzling and the view is golden.

The header

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

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

@end

The implementation file

#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

You will need to add my Swizz.h and Swizz.m

located Here

After that, you just Import the UIView+PassthroughParent.h in your {Project}-Prefix.pch file, and every view will have this ability.

every view will take points, but none of the blank space will.

I also recommend using a clear background.

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

EDIT

I created my own property bag, and that was not included previously.

Header file

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

Implementation File

// 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
Decrial answered 20/6, 2012 at 3:3 Comment(4)
passthroughPointInside method is working well for me (even without using any of the passthroughparent or swizz stuff - just rename passthroughPointInside to pointInside), thanks a lot.Viewer
What is propertyValueForKey?Benzine
Hmm. Nobody pointed that out before. I built a custom pointer dictionary that holds properties in a class extension. Ill see if I can find it to include it here.Decrial
Swizzling methods is known to be a delicate hacking solution for rare cases and developer fun. It's definitely not App Store safe because it is quite likely to break and crash your app with the next OS update. Why not just override pointInside: withEvent:?Viddah
S
1

Taking tips from the other answers and reading up on Apple's documentation, I created this simple library for solving your problem:
https://github.com/natrosoft/NATouchThroughView
It makes it easy to draw views in Interface Builder that should pass touches through to an underlying view.

I think method swizzling is overkill and very dangerous to do in production code because you are directly messing with Apple's base implementation and making an application-wide change that could cause unintended consequences.

There is a demo project and hopefully the README does a good job explaining what to do. To address the OP, you would change the clear UIView that contains the buttons to class NATouchThroughView in Interface Builder. Then find the clear UIView that overlays the menu that you want to be tap-able. Change that UIView to class NARootTouchThroughView in Interface Builder. It can even be the root UIView of your view controller if you intend those touches to pass through to the underlying view controller. Check out the demo project to see how it works. It's really quite simple, safe, and non-invasive

Sthilaire answered 22/10, 2013 at 11:35 Comment(0)
S
0

Try this

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
        }
    } 
Segalman answered 22/10, 2018 at 11:41 Comment(0)
D
0

Try set a backgroundColor of your transparentView as UIColor(white:0.000, alpha:0.020). Then you can get touch events in touchesBegan/touchesMoved methods. Place the code below somewhere your view is inited:

self.alpha = 1
self.backgroundColor = UIColor(white: 0.0, alpha: 0.02)
self.isMultipleTouchEnabled = true
self.isUserInteractionEnabled = true
Davidoff answered 22/1, 2019 at 22:27 Comment(0)
H
0

I use that instead of override method point(inside: CGPoint, with: UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }
Hanfurd answered 27/3, 2019 at 9:23 Comment(0)
K
-1

If you don't want to use a category or subclass UIView, you could also just bring the button forward so that it is in front of the transparent view. This won't always be possible depending on your application, but it worked for me. You can always bring the button back again or hide it.

Kristianson answered 10/5, 2015 at 15:36 Comment(2)
Hi, welcome to stack overflow. Just a hint for you as a new user: you might want to be careful of your tone. I'd avoid phrases like "if you can't bother"; it might appear as condescendingFuzee
Thanks for the heads up.. It was more from a lazy standpoint than condescending, but point taken.Kristianson

© 2022 - 2024 — McMap. All rights reserved.