UIGestureRecognizer blocks subview for handling touch events
Asked Answered
E

10

85

I'm trying to figure out how this is done the right way. I've tried to depict the situation: enter image description here

I'm adding a UITableView as a subview of a UIView. The UIView responds to a tap- and pinchGestureRecognizer, but when doing so, the tableview stops reacting to those two gestures (it still reacts to swipes).

I've made it work with the following code, but it's obviously not a nice solution and I'm sure there is a better way. This is put in the UIView (the superview):

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if([super hitTest:point withEvent:event] == self) {
        for (id gesture in self.gestureRecognizers) {
            [gesture setEnabled:YES];
        }
        return self;
    }
    for (id gesture in self.gestureRecognizers) {
        [gesture setEnabled:NO];
    }
    return [self.subviews lastObject];
}
Eisenhower answered 7/3, 2011 at 17:32 Comment(0)
P
184

I had a very similar problem and found my solution in this SO question. In summary, set yourself as the delegate for your UIGestureRecognizer and then check the targeted view before allowing your recognizer to process the touch. The relevant delegate method is:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
       shouldReceiveTouch:(UITouch *)touch
Princessprinceton answered 4/5, 2011 at 17:35 Comment(4)
I like this solution the most as it doesn't involve messing with the touches, hitTest:withEvent: or pointInside:withEvent:.Preussen
Clean solution, as you can test with e.g. return !(touch.view == givenView); if you just want to exclude a given view or return !(touch.view.tag == kTagNumReservedForExcludingViews); when you want to stop your recognizer processing the touch on a whole bunch of different subviews.Pellerin
I would do the hit test with - (BOOL)isDescendantOfView:(UIView *)view. This also works fine in - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event when subclassing UIGestureRecognizer.Fyke
@Princessprinceton , what if our gesture recognizer belongs to uiscrollview and we are unable to delegate the gesture recognizers?Condemnation
P
111

The blocking of touch events to subviews is the default behaviour. You can change this behaviour:

UITapGestureRecognizer *r = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(agentPickerTapped:)];
r.cancelsTouchesInView = NO;
[agentPicker addGestureRecognizer:r];
Perretta answered 20/7, 2012 at 6:5 Comment(3)
This will send the touch events to the subviews, but it will also be sent to the gesture recognizer. So this will prevent the blocking of the subviews but the gesture recognizer will still be recognized on subviews.Equilibrist
@Equilibrist Yes I agree with you. What I did in my gesture handler method is check whether the gesture location happens inside the concerned subview in which case I need not execute the remainder of the code. Also, one reason I chose this workaround is that UITapGestureRecognizer does not declare translationInView method. So implementing the UIGestureRecognizerDelegate method mentioned above would only result to crashing with error ... unrecognized selector sent to blah. To check, use something like: CGRectContainsPoint(subview.bounds, [recognizer locationInView:subview]).Peters
According to OP's question, this answer should be selected as main answer.Durstin
M
5

I was displaying a dropdown subview that had its own tableview. As a result, the touch.view would sometimes return classes like UITableViewCell. I had to step through the superclass(es) to ensure it was the subclass I thought it was:

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    UIView *view = touch.view;
    while (view.class != UIView.class) {
        // Check if superclass is of type dropdown
        if (view.class == dropDown.class) { // dropDown is an ivar; replace with your own
            NSLog(@"Is of type dropdown; returning NO");
            return NO;
        } else {
            view = view.superview;
        }
    }

    return YES;
}
Mimesis answered 24/7, 2013 at 11:43 Comment(1)
The while loop accounts for thatMimesis
I
5

Building on @Pin Shih Wang answer. We ignore all taps other than those on the view containing the tap gesture recognizer. All taps are forwarded to the view hierarchy as normal as we've set tapGestureRecognizer.cancelsTouchesInView = false. Here is the code in Swift3/4:

func ensureBackgroundTapDismissesKeyboard() {
    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
    tapGestureRecognizer.cancelsTouchesInView = false
    self.view.addGestureRecognizer(tapGestureRecognizer)
}

@objc func handleTap(recognizer: UIGestureRecognizer) {
    let location = recognizer.location(in: self.view)
    let hitTestView = self.view.hitTest(location, with: UIEvent())
    if hitTestView?.gestureRecognizers?.contains(recognizer) == .some(true) {
        // I dismiss the keyboard on a tap on the scroll view
        // REPLACE with own logic
        self.view.endEditing(true)
    }
}
Iodism answered 27/9, 2017 at 12:33 Comment(1)
How can I compare the gesture recognizer to a UISwipeActionStandardButton? I tried with .some(UISwipeActionStandardButton)Sporting
T
4

One possibility is to subclass your gesture recognizer (if you haven't already) and override -touchesBegan:withEvent: such that it determines whether each touch began in an excluded subview and calls -ignoreTouch:forEvent: for that touch if it did.

Obviously, you'll also need to add a property to keep track of the excluded subview, or perhaps better, an array of excluded subviews.

Thoreau answered 8/3, 2011 at 15:49 Comment(1)
There's heaps of code here github.com/…Agha
B
2

It is possible to do without inherit any class.

you can check gestureRecognizers in gesture's callback selector

if view.gestureRecognizers not contains your gestureRecognizer,just ignore it

for example

- (void)viewDidLoad
{
    UITapGestureRecognizer *singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self     action:@selector(handleSingleTap:)];
    singleTapGesture.numberOfTapsRequired = 1;
}

check view.gestureRecognizers here

- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer
{
    UIEvent *event = [[UIEvent alloc] init];
    CGPoint location = [gestureRecognizer locationInView:self.view];

    //check actually view you hit via hitTest
    UIView *view = [self.view hitTest:location withEvent:event];

    if ([view.gestureRecognizers containsObject:gestureRecognizer]) {
        //your UIView
        //do something
    }
    else {
        //your UITableView or some thing else...
        //ignore
    }
}
Bandicoot answered 14/4, 2014 at 5:9 Comment(1)
As mentioned in other answers, if using this method make sure the tap gesture recognizer forwards the tap to your view hierarchy with: singleTapGesture.cancelsTouchesInView = NO; added to viewDidLoad aboveIodism
A
1

I created a UIGestureRecognizer subclass designed for blocking all gesture recognizers attached to a superviews of a specific view.

It's part of my WEPopover project. You can find it here.

Archean answered 18/9, 2014 at 14:56 Comment(0)
Z
1

implement a delegate for all the recognizers of the parentView and put the gestureRecognizer method in the delegate that is responsible for simultaneous triggering of recognizers:

func gestureRecognizer(UIGestureRecognizer, shouldBeRequiredToFailByGestureRecognizer:UIGestureRecognizer) -> Bool {
    if (otherGestureRecognizer.view.isDescendantOfView(gestureRecognizer.view)) {
        return true
    } else {
        return false
    }
}

U can use the fail methods if u want to make the children be triggered but not the parent recognizers:

https://developer.apple.com/reference/uikit/uigesturerecognizerdelegate

Zyrian answered 12/12, 2016 at 11:1 Comment(0)
D
0

You can turn it off and on.... in my code i did something like this as i needed to turn it off when the keyboard was not showing, you can apply it to your situation:

call this is viewdidload etc:

NSNotificationCenter    *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(notifyShowKeyboard:) name:UIKeyboardDidShowNotification object:nil];
[center addObserver:self selector:@selector(notifyHideKeyboard:) name:UIKeyboardWillHideNotification object:nil];

then create the two methods:

-(void) notifyShowKeyboard:(NSNotification *)inNotification 
{
    tap.enabled=true;  // turn the gesture on
}

-(void) notifyHideKeyboard:(NSNotification *)inNotification 
{
    tap.enabled=false;  //turn the gesture off so it wont consume the touch event
}

What this does is disables the tap. I had to turn tap into a instance variable and release it in dealloc though.

Durango answered 1/12, 2012 at 4:12 Comment(0)
S
0

I was also doing a popover and this is how I did it

func didTap(sender: UITapGestureRecognizer) {

    let tapLocation = sender.locationInView(tableView)

    if let _ = tableView.indexPathForRowAtPoint(tapLocation) {
        sender.cancelsTouchesInView = false
    }
    else {
        delegate?.menuDimissed()
    }
}
Syllabic answered 29/10, 2015 at 20:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.