195

I have view with a UITapGestureRecognizer. So when I tap on the view another view appears above this view. This new view has three buttons. When I now press on one of these buttons I don't get the buttons action, I only get the tap gesture action. So I'm not able to use these buttons anymore. What can I do to get the events through to these buttons? The weird thing is that the buttons still get highlighted.

I can't just remove the UITapGestureRecognizer after I received it's tap. Because with it the new view can also be removed. Means I want a behavior like the fullscreen vide controls.

TheNeil
  • 3,321
  • 2
  • 27
  • 52
V1ru8
  • 6,139
  • 4
  • 30
  • 46

12 Answers12

263

You can set your controller or view (whichever creates the gesture recognizer) as the delegate of the UITapGestureRecognizer. Then in the delegate you can implement -gestureRecognizer:shouldReceiveTouch:. In your implementation you can test if the touch belongs to your new subview, and if it does, instruct the gesture recognizer to ignore it. Something like the following:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    // test if our control subview is on-screen
    if (self.controlSubview.superview != nil) {
        if ([touch.view isDescendantOfView:self.controlSubview]) {
            // we touched our control surface
            return NO; // ignore the touch
        }
    }
    return YES; // handle the touch
}
Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • In my header file, i had my view implement UIGestoreRegognizerDelegate, and in my .m i added your code above. The tap never enters this method, it goes straight to my handler. Any ideas? – kmehta Apr 29 '11 at 21:50
  • 1
    @kmehta you most likely forgot to set the UIGestureRecognizer delegate property. – Till May 10 '11 at 01:38
  • Can this answer be generalized to handle all cases where there is an IBAction involved? – Martin Wickman Aug 03 '11 at 08:54
  • @Martin IBAction compiles down to `void` so it doesn't leave any information behind at runtime to use for detection. But what you can do is walk up the view hierarchy, testing for `UIControl`, and returning `NO` if you find any controls. – Lily Ballard Aug 03 '11 at 17:38
  • thx for this, it helps me. if your button is in a UIImageView, make sure if userInteractionEnabled of the imageView is set at YES. – james075 Feb 21 '13 at 02:32
160

As a follow up to Casey's follow up to Kevin Ballard's answer:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
        if ([touch.view isKindOfClass:[UIControl class]]) {
            // we touched a button, slider, or other UIControl
            return NO; // ignore the touch
        }
    return YES; // handle the touch
}

This basically makes all user input types of controls like buttons, sliders, etc. work

cdasher
  • 3,083
  • 1
  • 19
  • 20
  • 1
    That's a flexible solution that can be accompanied with ```setCancelsTouchesInView = NO``` to not trigger tap gesture on interaction with controls. However you can write it better: ```return ![touch.view isKindOfClass:[UIControl class]];``` – griga13 Mar 22 '17 at 17:54
  • 2
    Swift 4+ solution: return !(touch.view is UIControl) – nteissler Feb 04 '19 at 15:25
108

Found this answer here: link

You can also use

tapRecognizer.cancelsTouchesInView = NO;

Which prevents the tap recognizer to be the only one to catch all the taps

UPDATE - Michael mentioned the link to the documentation describing this property: cancelsTouchesInView

Community
  • 1
  • 1
ejazz
  • 2,458
  • 1
  • 19
  • 29
  • 9
    This should be the accepted answer IMO. This lets every subview handle the tap on its own and prevents the view which has a tabrecognizer attached to it from 'highjacking' the tap. No need to implement a delegate if all you want to do is make a view clickable (to resign first responder / hide the keyboard on textfields etc).. awesome! – EeKay Jul 10 '12 at 08:48
  • 4
    This should definitely be the accepted answer. This answer led me to read the [Apple documentation](http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIGestureRecognizer_Class/Reference/Reference.html) properly, which makes it clear that the gesture recogniser will prevent subviews from getting the recognised events _unless_ you do this. – Michael van der Westhuizen Aug 05 '12 at 09:21
  • 1
    This used to work but now it is not working for me..perhaps because I have a scrollView under the button? I had to implement the delegate like in the answers above. – xissburg Aug 15 '12 at 00:48
  • 7
    After experimenting a little, I noticed that this will only work if the view which contains the gesture recognizer is a sibling of the UIControl, and it won't if the view is the parent of the UIControl. – xissburg Aug 15 '12 at 00:54
  • This is the best. Who knew that tap recognizers by default eat touches on the view? Who'd have guessed. – n13 Feb 09 '13 at 04:29
  • 9
    Not really work as intended... YES, it's prevent tap recognizer to eat the event on UIControl. But it's still run the recognizer action, which is run 2 action at the same time. – Tek Yin Nov 11 '13 at 07:37
72

As a follow up to Kevin Ballard's answer, I had this same problem and ended up using this code:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    if ([touch.view isKindOfClass:[UIButton class]]){
        return NO;
    }
    return YES;
}

It has the same effect but this will work on any UIButton at any view depth (my UIButton was several views deep and the UIGestureRecognizer's delegate didn't have a reference to it.)

jbrennan
  • 11,943
  • 14
  • 73
  • 115
Casey
  • 2,393
  • 1
  • 20
  • 22
  • 6
    Don't wanna be a dick here, but it's really YES and NO. Please use Cocoa conventions. TRUE/FALSE/NULL is for the CoreFoundation-Layer. – steipete Dec 20 '11 at 20:10
  • from what i've read, YES and NO were actually created for readability. readability is subjective. it stems from the method and property naming conventions which not everyone is overly concerned about. if you want strict adherence to naming convention then yes use YES and NO for any NSWhatever. however, i will say that if you survey developers you WILL get mixed opinions on which one of these is more readable [button setHidden:YES]; -or- [button setHidden:TRUE]; – Pimp Juice McJones Dec 21 '11 at 17:59
  • 1
    i guess what i'm trying to say is that if the premise of the naming conventions are readability then I think it's hard to deny that it's subjective in some cases. if you're a purist then you won't understand that statement. – Pimp Juice McJones Dec 21 '11 at 18:07
  • 2
    It may seem subjective but after a while of Cocoa programming you really get into the more semantically accurate flow of code... the "TRUE" just seems really wrong now after years of Objective-C, because it does not match "hidden" very well. – Kendall Helmstetter Gelner Jun 26 '12 at 03:29
  • Since when can/do you pass a tap gesture to a UIButton as a Touch Up Inside UITouch Event (the latter being the event generated by tapping a UIButton)? It seems duplicative and *incorrect* to create a tap gesture recognizer for a button that has 15 touch events already associated with a tap gesture. I'd like to see code, not guesses. – James Bush Oct 07 '19 at 12:07
11

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:.....

From Apple's API doc

Jagie
  • 2,190
  • 3
  • 27
  • 25
8

These answers were incomplete. I had to read multiple posts as to how to use this boolean operation.

In your *.h file add this

@interface v1ViewController : UIViewController <UIGestureRecognizerDelegate>

In your *.m file add this

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {

    NSLog(@"went here ...");

    if ([touch.view isKindOfClass:[UIControl class]])
    {
        // we touched a button, slider, or other UIControl
        return NO; // ignore the touch
    }
    return YES; // handle the touch
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.



    //tap gestrure
    UITapGestureRecognizer *tapGestRecog = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(screenTappedOnce)];
    [tapGestRecog setNumberOfTapsRequired:1];
    [self.view addGestureRecognizer:tapGestRecog];


// This line is very important. if You don't add it then your boolean operation will never get called
tapGestRecog.delegate = self;

}


-(IBAction) screenTappedOnce
{
    NSLog(@"screenTappedOnce ...");

}
Sam B
  • 27,273
  • 15
  • 84
  • 121
7

Swift 5

Button on superview with tapgesture

 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
    if let _ = touch.view as? UIButton { return false }
    return true
}

In my case implementing hitTest worked for me. I had collection view with button

This method traverses the view hierarchy by calling the point(inside:with:) method of each subview to determine which subview should receive a touch event. If point(inside:with:) returns true, then the subview’s hierarchy is similarly traversed until the frontmost view containing the specified point is found.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard isUserInteractionEnabled else { return nil }

    guard !isHidden else { return nil }

    guard alpha >= 0.01 else { return nil }

    guard self.point(inside: point, with: event) else { return nil }

    for eachImageCell in collectionView.visibleCells {
        for eachImageButton in eachImageCell.subviews {
            if let crossButton = eachImageButton as? UIButton {
                if crossButton.point(inside: convert(point, to: crossButton), with: event) {
                    return crossButton
                }
            }
        }
    }
    return super.hitTest(point, with: event)
}
Shruthi Pal
  • 89
  • 1
  • 3
7

Found another way to do it from here. It detects the touch whether inside each button or not.

(1) pointInside:withEvent: (2) locationInView:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 
       shouldReceiveTouch:(UITouch *)touch {
    // Don't recognize taps in the buttons
    return (![self.button1 pointInside:[touch locationInView:self.button1] withEvent:nil] &&
            ![self.button2 pointInside:[touch locationInView:self.button2] withEvent:nil] &&
            ![self.button3 pointInside:[touch locationInView:self.button3] withEvent:nil]);
}
Community
  • 1
  • 1
MindMirror
  • 71
  • 1
  • 1
  • I tried all of the other solutions for this, but the -pointInside:withEvent: approach was the only one that worked for me. – Kenny Wyland Sep 17 '14 at 03:09
3

Here's the Swift version of Lily Ballard's answer that worked for me:

func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
    if (scrollView.superview != nil) {
        if ((touch.view?.isDescendantOfView(scrollView)) != nil) { return false }
    }
    return true
}
jscs
  • 63,694
  • 13
  • 151
  • 195
John Riselvato
  • 12,854
  • 5
  • 62
  • 89
1

You can stop the UITapGestureRecognizer from cancelling other events (such as the tap on your button) by setting the following boolean:

    [tapRecognizer setCancelsTouchesInView:NO];
Himanshu Mahajan
  • 4,779
  • 2
  • 36
  • 29
1

If your scenario is like this:

You have a simple view and some UIButtons,UITextField controls added as subviews to that view. Now you want to dismiss the keyboard when you touch anywhere else on the view except on the controls(subviews you added)

Then Solution is:

Add the following method to your XYZViewController.m(which has your view)

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.view endEditing:YES];
}
NaveenRaghuveer
  • 124
  • 1
  • 8
  • First I tried with _@cdasher_'s answer but in my case even on clicking on button, Iam getting **touch.view** as my "view" in ViewController but not my "UIButton" control. Can someone say why the touch's view property is not returning the original view in which tap happened – NaveenRaghuveer May 23 '13 at 06:52
  • 1
    Will not work if you have a scrollview or some other view that claims the touch events. – lkraider Aug 10 '16 at 17:28
1

Optimizing cdasher's answer, you get

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
       shouldReceiveTouch:(UITouch *)touch 
{
    return ![touch.view isKindOfClass:[UIControl class]];
}
jscs
  • 63,694
  • 13
  • 151
  • 195
Clay Bridges
  • 11,602
  • 10
  • 68
  • 118