201

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.

TheNeil
  • 3,321
  • 2
  • 27
  • 52
Sean Clark Hess
  • 15,859
  • 12
  • 52
  • 100
  • 1
    I thought transparent (alpha 0) UIViews aren’t supposed to respond to touch events? – Evadne Wu Jun 16 '10 at 00:56
  • 1
    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. – Segev Apr 08 '15 at 10:48
  • *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. – heyfrank Oct 28 '22 at 09:27

16 Answers16

340

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.

John Stephen
  • 7,625
  • 2
  • 31
  • 45
  • 5
    Interesting. I need to dive into the responder chain more. – Sean Clark Hess Oct 26 '10 at 15:23
  • 1
    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. – The Lazy Coder Jun 20 '12 at 02:56
  • 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. – John Stephen Jun 22 '12 at 20:35
  • 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. – John Stephen Jun 22 '12 at 20:37
  • 2
    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 ? – cyrilchampier Aug 15 '12 at 16:15
  • 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. – KyleStew May 17 '13 at 23:14
  • 1
    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. – James Andrews Dec 03 '13 at 17:19
  • 2
    I did a swift Version: maybe you can include it in the answer for visibility https://gist.github.com/eyeballz/17945454447c7ae766cb – kutschenator Jan 31 '15 at 08:00
  • 1
    this answer really help me, note: you need to set PassThroughView in both Container and Embed View – Sruit A.Suk Mar 08 '16 at 02:43
  • @cyrilchampier had the same problem and found a solution that works for me, check my answer down below, basically consist in overriding hitTest – PakitoV Feb 12 '18 at 11:33
  • You should also implement - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { – monkeydom Mar 06 '19 at 08:14
125

I also use

myView.userInteractionEnabled = NO;

No need to subclass. Works fine.

akw
  • 2,090
  • 1
  • 15
  • 21
  • 96
    This will also disable user interaction for any subviews – pixelfreak Aug 15 '13 at 10:03
  • 1
    As @pixelfreak 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. – Augusto Carmo Nov 29 '18 at 12:31
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).

jackslash
  • 8,550
  • 45
  • 56
  • 1
    It would be awesome to have a link to the docs you're quoting, along with the quotes themselves. – morgancodes Jul 12 '12 at 20:07
  • 1
    I added the links to the relevant places in the documentation for you – jackslash Jul 13 '12 at 10:56
  • 1
    ~~Could you show how that override would have to look like?~~ Found a [answer in another question](http://stackoverflow.com/a/12355957/999162) of how this would look like; it's literally just returning `NO`; – kontur Jun 09 '15 at 14:18
  • This should probably be the accepted answer. It's the simplest way and the recommended way. – Ecuador Mar 28 '20 at 16:52
8

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

madx
  • 6,723
  • 4
  • 55
  • 59
Jeff
  • 840
  • 10
  • 29
  • 13
    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. – auco Aug 04 '16 at 08:44
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
    }
}
PakitoV
  • 2,476
  • 25
  • 34
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

Segev
  • 19,035
  • 12
  • 80
  • 152
  • 2
    While this solution may work, it's better to place the relevant parts of your solution within the answer. – Koen. Jul 12 '18 at 13:16
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;
}
Tod Cunningham
  • 3,691
  • 4
  • 30
  • 32
  • Need to check if the view is visible, and if the subviews are visible. – The Lazy Coder Jun 20 '12 at 02:57
  • 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. – auco Aug 04 '16 at 08:53
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
}
Barath
  • 1,656
  • 19
  • 19
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?

LK.
  • 4,499
  • 8
  • 38
  • 48
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
Community
  • 1
  • 1
The Lazy Coder
  • 11,560
  • 4
  • 51
  • 69
  • 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. – Benjamin Piette Mar 08 '13 at 15:29
  • What is propertyValueForKey? – Skotch Oct 31 '13 at 17:34
  • 1
    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. – The Lazy Coder Oct 31 '13 at 17:37
  • 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:`? – auco Aug 04 '16 at 08:48
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

n8tr
  • 5,018
  • 2
  • 32
  • 33
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.

Liam
  • 7,762
  • 4
  • 26
  • 27
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
        }
    } 
tBug
  • 789
  • 9
  • 13
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
Agisight
  • 1,778
  • 1
  • 14
  • 15
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
    }
Wei
  • 324
  • 3
  • 10
-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.

Shaked Sayag
  • 5,712
  • 2
  • 31
  • 38
  • 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 condescending – nomistic May 10 '15 at 15:41
  • Thanks for the heads up.. It was more from a lazy standpoint than condescending, but point taken. – Shaked Sayag May 11 '15 at 12:37