186
votes

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.

16
I thought transparent (alpha 0) UIViews aren’t supposed to respond to touch events?Evadne Wu
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

16 Answers

327
votes

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.

117
votes

I also use

myView.userInteractionEnabled = NO;

No need to subclass. Works fine.

20
votes

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

8
votes

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

7
votes

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

6
votes

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;
}
6
votes

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

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
votes

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?

1
votes

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.

1
votes

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

1
votes

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
0
votes

If you can't bother 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.

0
votes

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

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
0
votes

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
    }