171

I have a view overlayed on top of many other views. I am only using the overaly to detect some number of touches on the screen, but other than that I don't want the view to stop the behavior of other views underneath, which are scrollviews, etc. How can I forward all the touches through this overlay view? It is a subclass of UIView.

Mikael Engver
  • 4,634
  • 4
  • 46
  • 53
sol
  • 6,402
  • 13
  • 47
  • 57
  • 3
    Have you solved your problem? If yes then Please accept answer so that its easier for other to find the solution because I am facing the Same issue. – Khushbu Desai Aug 10 '18 at 06:41
  • this answer works good https://stackoverflow.com/a/4010809/4833705 – Lance Samaria Mar 05 '19 at 15:36
  • On the off-chance that your problem is your view is behind a UINavigationBar (which you can observe by by using XCode's 3D view-stack debugger, in the debugging section (icon of three little squares stacked)), then the technique of subclassing UIView to make it pass through passthrough might not be enough (or even really what the problem is!). But I stumbled on this answer which overrides methods in UINavigationBar as an extension and it works brilliantly for the Navigation Bar blocking touches problem. https://stackoverflow.com/a/61312628/2079103 – clearlight Apr 30 '22 at 23:23

13 Answers13

143

Disabling user interaction was all I needed!

Objective-C:

myWebView.userInteractionEnabled = NO;

Swift:

myWebView.isUserInteractionEnabled = false
rmp251
  • 5,018
  • 4
  • 34
  • 46
  • 1
    This worked in my situation where I had a subview of the button. I disabled interaction on the subview and the button worked. – simple_code Dec 07 '18 at 08:53
  • 3
    In Swift 4, `myWebView.isUserInteractionEnabled = false` – Louis 'LYRO' Dupont Sep 02 '19 at 13:46
  • 1
    Be aware, using this solution also removes the ability for Voice Over to read any accessibility labels from the element, if for example it's a label. – Wez Mar 03 '22 at 16:33
134

For passing touches from an overlay view to the views underneath, implement the following method in the UIView:

Objective-C:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"Passing all touches to the next view (if any), in the view stack.");
    return NO;
}

Swift 5:

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    print("Passing all touches to the next view (if any), in the view stack.")
    return false
}
PixelCloudSt
  • 2,805
  • 1
  • 18
  • 19
  • 5
    I have same issue, but what I want is if I am zooming/rotating/moving view with gesture then it should return yes and for rest event it should return no, how can I achieve this? – Minakshi Aug 12 '13 at 06:27
  • 1
    @Minakshi that's a totally different question, ask a new question for that. – Fattie Oct 15 '19 at 21:53
  • 1
    but be aware this simple approach DOES NOT LET YOU HAVE buttons and so on ON TOP OF the layer in question! – Fattie Oct 15 '19 at 22:30
  • 1
    @Minakshi hi, did you find an answer to your question? – Lance Samaria Aug 09 '20 at 07:32
65

This is an old thread, but it came up on a search, so I thought I'd add my 2c. I have a covering UIView with subviews, and only want to intercept the touches that hit one of the subviews, so I modified PixelCloudSt's answer to:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    for (UIView* subview in self.subviews ) {
        if ( [subview hitTest:[self convertPoint:point toView:subview] withEvent:event] != nil ) {
            return YES;
        }
    }
    return NO;
}
Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
fresidue
  • 888
  • 8
  • 10
  • 1
    You need to create a custom UIView subclass (or a subclass of any other UIView subclass such as UIImageView or whatever) and override this function. In essence, the subclass can have an entirely empty '@interface' in the .h file, and the function above be the entire contents of the '@implementation'. – fresidue Mar 07 '14 at 16:01
  • 1
    Thanks, this is exactly what I needed to pass touches through the empty space of my UIView, to be handled by the view behind. +100 – Danny Jun 30 '20 at 00:41
  • I just wanted to upvote this one again after I guess 2 years....... – bojan Sep 17 '21 at 21:58
  • This is exactly what I was looking for. Also works great in Swift 5.6 (after converting)! – erik_m_martens Apr 09 '22 at 09:01
58

Improved version of @fresidue answer. You can use this UIView subclass as transparent view passing touches outside its subview. Implementation in 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 pointInside:[self convertPoint:point toView:view] withEvent:event]) {
            return YES;
        }
    }
    return NO;
}

@end

.

and in Swift:

class PassthroughView: UIView {
  override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    return subviews.contains(where: {
      !$0.isHidden
      && $0.isUserInteractionEnabled
      && $0.point(inside: self.convert(point, to: $0), with: event)
    })
  }
}

TIP:

Say then you have a large "holder" panel, perhaps with a table view behind. You make the "holder" panel PassthroughView. It will now work, you can scroll the table "through" the "holder".

But!

  1. On top of the "holder" panel you have some labels or icons. Don't forget, of course those must simply be marked user interaction enabled OFF!

  2. On top of the "holder" panel you have some buttons. Don't forget, of course those must simply be marked user interaction enabled ON!

  3. Note that somewhat confusingly, the "holder" itself - the view you use PassthroughView on - must be marked user interaction enabled ON! That's ON!! (Otherwise, the code in PassthroughView simply will never be called.)

Community
  • 1
  • 1
Timur Bernikovich
  • 5,660
  • 4
  • 45
  • 58
12

I needed to pass touches through a UIStackView. A UIView inside was transparent, but the UIStackView consumed all touches. This worked for me:

class PassThrouStackView: UIStackView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        if view == self {
            return nil
        }
        return view
    }
}

All arrangedSubviews still receive touches, but touches on the UIStackView itself went through to the view below (for me a mapView).

gorkem
  • 500
  • 6
  • 8
  • Just wanted to add you can apply this by extending UIView as well. Here's an article using this exact same method that goes into a bit more detail as well as other use cases: https://medium.com/@nguyenminhphuc/how-to-pass-ui-events-through-views-in-ios-c1be9ab1626b – Kacy Sep 07 '20 at 23:26
  • is it possible for UIScrollview – Midhun Narayan Sep 22 '21 at 11:21
8

I had a similar issue with a UIStackView (but could be any other view). My configuration was the following: View controller with containerView and StackView

It's a classical case where I have a container that needed to be placed in the background, with buttons on the side. For layout purposes, I included the buttons in a UIStackView, but now the middle (empty) part of the stackView intercepts touches :-(

What I did is create a subclass of UIStackView with a property defining the subView that should be touchable. Now, any touch on the side buttons (included in the * viewsWithActiveTouch* array) will be given to the buttons, while any touch on the stackview anywhere else than these views won't be intercepted, and therefore passed to whatever is below the stack view.

/** Subclass of UIStackView that does not accept touches, except for specific subviews given in the viewsWithActiveTouch array */
class NoTouchStackView: UIStackView {

  var viewsWithActiveTouch: [UIView]?

  override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    if let activeViews = viewsWithActiveTouch {
        for view in activeViews {
           if CGRectContainsPoint(view.frame, point) {
                return view

           }
        }
     }
     return nil
   }
}
Frederic Adda
  • 5,905
  • 4
  • 56
  • 71
6

If the view you want to forward the touches to doesn't happen to be a subview / superview, you can set up a custom property in your UIView subclass like so:

@interface SomeViewSubclass : UIView {

    id forwardableTouchee;

}
@property (retain) id forwardableTouchee;

Make sure to synthesize it in your .m:

@synthesize forwardableTouchee;

And then include the following in any of your UIResponder methods such as:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    [self.forwardableTouchee touchesBegan:touches withEvent:event];

}

Wherever you instantiate your UIView, set the forwardableTouchee property to whatever view you'd like the events to be forwarded to:

    SomeViewSubclass *view = [[[SomeViewSubclass alloc] initWithFrame:someRect] autorelease];
    view.forwardableTouchee = someOtherView;
Chris Ladd
  • 2,795
  • 1
  • 29
  • 22
5

In Swift 5

class ThroughView: UIView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        guard let slideView = subviews.first else {
            return false
        }

        return slideView.hitTest(convert(point, to: slideView), with: event) != nil
    }
}
onmyway133
  • 45,645
  • 31
  • 257
  • 263
4

Looks like even thou its quite a lot of answers here, there is no one clean in swift that I needed. So I took answer from @fresidue here and converted it to swift as it's what now mostly developers want to use here.

It solved my problem where I have some transparent toolbar with button but I want toolbar to be invisible to user and touch events should go through.

isUserInteractionEnabled = false as some stated is not an option based on my testing.

 override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    for subview in subviews {
        if subview.hitTest(convert(point, to: subview), with: event) != nil {
            return true
        }
    }
    return false
}
Renetik
  • 5,887
  • 1
  • 47
  • 66
  • That's my experience too. So I'm testing the point approach. In fact the Apple docs say for `.isUserInteractionEnabled` that if false events intended for the view are removed from the event view, and clearly a touch on a view is intended for the view. – clearlight Apr 30 '22 at 22:59
2

I had couple of labels inside StackView and I didn't have much success with the solutions above, instead I solved my problem using below code:

let item = ResponsiveLabel()

// Configure label

stackView.addArrangedSubview(item)

Subclassing UIStackView:

class PassThrouStackView:UIStackView{
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        for subview in self.arrangedSubviews {
            let convertedPoint = convert(point, to: subview)
            let labelPoint = subview.point(inside: convertedPoint, with: event)
            if (labelPoint){
                return subview
            }
            
        }
        return nil
    }
    
}

Then you could do something like:

class ResponsiveLabel:UILabel{
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // Respond to touch
    }
}
Olcay Ertaş
  • 5,987
  • 8
  • 76
  • 112
Amir.n3t
  • 2,859
  • 3
  • 21
  • 28
0

Try something like this...

for (UIView *view in subviews)
  [view touchesBegan:touches withEvent:event];

The code above, in your touchesBegan method for example would pass the touches to all of the subviews of view.

Jordan
  • 21,746
  • 10
  • 51
  • 63
  • 7
    The problem with this approach AFAIK is that attempting to forward touches to the classes of UIKit framework will most often have no effect. According to Apple's documentation UIKit framework classes are not designed to recieve touches that have the view property set to some other view. So this approach works mostly for custom subclasses of UIView. – maciejs Mar 02 '11 at 23:05
0

The situation I was trying to do was build a control panel using controls inside nested UIStackView’s. Some of the controls had UITextField’s others with UIButton’s. Also, there were labels to identify the controls. What I wanted to do was put a big “invisible” button behind the control panel so that if a user tapped on an area outside a button or text field, that I could then catch that and take action - primarily dismiss any keyboard if a text field was active (resignFirstResponder). However, tapping on a label or other blank area in the control panel would not pass things through. The above discussions were helpful in coming up with my answer below.

Basically, I sub-classed UIStackView and overwrote the “point(inside:with) routine to look for the type of controls that needed the touch and “ignore” things like labels that I wanted to ignore. It also checks for inside UIStackView’s so that things can recurse into the control panel structure.

The code is a perhaps a little more verbose than it should be. But it was helpful in debugging and hopefully provides more clarity in what the routine is doing. Just be sure in Interface Builder to change the class of the UIStackView's to PassThruStack.

class PassThruStack: UIStackView {

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {

    for view in self.subviews {
        if !view.isHidden {
            let isStack = view is UIStackView
            let isButton = view is UIButton
            let isText = view is UITextField
            if isStack || isButton || isText {
                let pointInside = view.point(inside: self.convert(point, to: view), with: event)
                if pointInside {
                    return true
                }
            }
        }
    }
    return false
}

}

anorskdev
  • 1,867
  • 1
  • 15
  • 18
-1

As suggested by @PixelCloudStv if you want to throw touched from one view to another but with some additional control over this process - subclass UIView

//header

@interface TouchView : UIView

@property (assign, nonatomic) CGRect activeRect;

@end

//implementation

#import "TouchView.h"

@implementation TouchView

#pragma mark - Ovverride

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    BOOL moveTouch = YES;
    if (CGRectContainsPoint(self.activeRect, point)) {
        moveTouch = NO;
    }
    return moveTouch;
}

@end

After in interfaceBuilder just set class of View to TouchView and set active rect with your rect. Also u can change and implement other logic.

hbk
  • 10,908
  • 11
  • 91
  • 124