How to ignore touch events and pass them to another subview's UIControl objects?
Asked Answered
C

7

76

I have a custom UIViewController whose UIView takes up a corner of the screen, but most of it is transparent except for the parts of it that have some buttons and stuff on it. Due to the layout of the objects on that view, the view's frame can cover up some buttons beneath it. I want to be able to ignore any touches on that view if they aren't touching anything important on it, but I seem to only be able to pass along actual touch events (touchesEnded/nextResponder stuff). If I have a UIButton or something like that which doesnt use touchesEnded, how do I pass the touch event along to that?

I can't just manually figure out button selector to call, because this custom ViewController can be used on many different views. I basically need a way to call this:

[self.nextResponder touchesEnded:touches withEvent:event];

on UIControl types as well.

Cataract answered 10/10, 2011 at 22:30 Comment(0)
K
139

Probably the best way to do this is to override hitTest:withEvent: in the view that you want to be ignoring touches. Depending on the complexity of your view hierarchy, there are a couple of easy ways to do this.

If you have a reference to the view underneath the view to ignore:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];

    // If the hitView is THIS view, return the view that you want to receive the touch instead:
    if (hitView == self) {
        return otherView;
    }
    // Else return the hitView (as it could be one of this view's buttons):
    return hitView;
}

If you don't have a reference to the view:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];

    // If the hitView is THIS view, return nil and allow hitTest:withEvent: to
    // continue traversing the hierarchy to find the underlying view.
    if (hitView == self) {
        return nil;
    }
    // Else return the hitView (as it could be one of this view's buttons):
    return hitView;
}

I would recommend the first approach as being the most robust (if it's possible to obtain a reference to the underlying view).

Karaite answered 10/10, 2011 at 23:42 Comment(6)
That seems like what I want to do, but the view I want ignoring touches is the view property of a UIViewController I created, so is there a way to override the hitTest function of that view?Cataract
@chris: Sure, just subclass it and throw in the overridden method as shown above. It means you'll have an extra .h and .m file hanging around with very little code in it, but it will do the job. Besides, a passthrough view is a useful subclass to have in your toolkit. If your view controller creates the view in a nib, just going into the IB file and change the view's class to your new subclass in the identity inspector. If it's created programmatically, then create and assign your view subclass in loadView (see edit).Karaite
Wonderful advise! I've been searching for a good solution for a whole day. Thanks.Seritaserjeant
Works well however i would like the otherView to be returned only if the event is a tap event, not a drag event. Is there a way to differentiate the different kind of events?Liam
@htafoya: It sounds like gesture recognizers are what you need there. Gestures recognizers for a particular gesture (e.g. a tap) are only handled by the view to which they are attached. In general you should use touch handling (hitTest:withEvent:) to decide whether a view should receive touches at all, but use gesture recognizers to distinguish between different types of touch.Karaite
Fantastic solution! Something that came in handy for me was to use if hitView is UIControl to test if the hit was on a button (would work on any type of control). If so I return the view otherwise, return nil to pass the touch through.Jamijamie
L
18

Swift answer:

You can create a UIView subclass, IgnoreTouchView. In a storyboard, set it on the VC's view(s) you want to pass through touches:

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

Note that you must set UserInteractionEnabled to true, for the UIView in question!

Lollygag answered 2/1, 2017 at 23:25 Comment(1)
Kevin, I edited a few characters to the latest Swift version, April2017. (Feel free to change etc.) CheersMirna
F
18

You could also disable user interaction so that touch events are ignored and are passed to the parent.

In Swift:

yourView.isUserInteractionEnabled = false
Flagellant answered 28/8, 2018 at 9:52 Comment(1)
I don't think that's right. If isUserInteractionEnabled is true, then events are ignored and (according to Apple docs) "removed from the event queue". ie they won't get passed to the next view as the OP wants.Halmahera
P
8

I haven't found a good way to pass UITouch events between objects. What usually works better is to implement hitTest and return nil if the point isn't in the view that you want to handle it:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
Paff answered 10/10, 2011 at 23:37 Comment(0)
K
4

This is an old question, and it comes up at the top of searches. So, I thought I'd post another possible solution that might work better for your situation. In my case I wanted to drag a label on top of another label and I wanted to get the label below in the hitTest. The simplest thing to do is to set the userInteractionEnabled to NO or false, then do your hitTest, and then set it back again if you need to move it again. Here's a code sample in Swift:

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    let touch = touches.first
    let location = touch?.locationInView(self.view)
    self.label1.userInteractionEnabled = false
    let view = self.view.hitTest(location!, withEvent: nil) as? UILabel
    print(view?.text)
    self.label1.userInteractionEnabled = true
}
Kasandrakasevich answered 28/2, 2016 at 3:0 Comment(8)
Thanks for the solution.Calfskin
That won't work because once you set the label1.userInteractionEnabled to false, then the label1 won't even detect touches later.Northman
@FredA. That's why I wrote: "... and then set it back again if you need to move it again". Maybe I wasn't clear enough?Kasandrakasevich
Yes, you could set it back again, but not inside touchesEnded. You can set it elsewhere, but not inside a touch method.Northman
@FredA. It works. I'm not sure what the problem is. TouchesEnded. Disable user interaction. Do the hittest. Set interaction back to enable. It's a well documented way of doing it. You must be doing something differently.Kasandrakasevich
You can't do the hit test AFTER having disabled user interaction. Just tried it.Northman
@FredA. Like I said, this is 1) a well known and documented way of doing it, and 2) I'm sitting here looking at a project that uses it! So you are obviously doing something wrong. Without more information I can't really provide direction. Can I make a suggestion? Try adopting a more constructive approach if you want to get help on SO. I'm happy to assist, but suggesting my code isn't working instead of assuming you're doing something wrong isn't that helpful a stance.Kasandrakasevich
Ok I'm sorry I was disrespectful towards your code. Won't happen again.Northman
T
3

My problem was similar: I had a UIControl with complex touch responders, but it acted funny when embedded in a scroll view...

This prevents parent scrolling (or can be modified to prevent other interactions) when sub view has an active touch gesture.

My solution:

Create a delegate protocol for didBeginInteraction and didEndInteraction in sub view.

Call delegate.didBeginInteraction from touchesBegan

Call delegate.didEndInteraction from touchesEnded and touchesCancelled

In parent controller, implement didBegin and didEnd protocol, set delegate

Then set scrollView.scrollEnabled = NO in didBegin, scrollEnabled = YES in didEnd.

Hope this helps someone with a slightly different use case than the question posed!

Tails answered 14/2, 2017 at 21:40 Comment(1)
Wow, I did not think I'd find the solution to the exact niche problem I was having with my custom UIControl. Thanks man!Runkel
R
1

Easies solution for me was setting isUserInteractionEnabled to false on specific subviews added to a button.

// Some example view components
let button: UIButton = ...
let stackView: UIStackView = ...

// Setting this to false will make all touches ignored by this subview
stackView.isUserInteractionEnabled = false

// Maybe this was my specific case, but I had a pretty custom button with a bunch of subviews.
button.addSubview(stackView)
Ravage answered 26/11, 2019 at 13:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.