33

I have a UIView (the 'container view') which contains several 'sub views'. I want to add a UITapGestureRecognizer to the container view, such that it is activated when I touch the region inside the container view but outside the subviews.

At the moment, touching anywhere inside the container view, including inside the subviews activates the gesture recognizer.

The implementation looks something like this: In the controller:

ContainerView *containerView = [[ContainerView alloc] initWithSubViews:array];
UITapGestureRecognizer *tap = [UITapGestureRecognizer alloc] initWithTarget:self action:@selector(someSelector)];
[containerView addGestureRecognizer:tap];
[self.view addSubView:containerView];

In ContainerView.m

-(id)initWithSubviews:(NSArray *)array {
    for (subView *s in array) {
        [self addSubView:s];
    }
    return self;
}

I think the problem occurs because the gesture recognizer is added after the subviews are. If that is true then the solution would require breaking the initWithSubViews method into two separate ones, which I would prefer to avoid.

Thank You

Awais Hussain
  • 1,501
  • 1
  • 14
  • 15

9 Answers9

42

I used the simple way below. It works perpectly!

Implement UIGestureRecognizerDelegate function, accept only touchs on superview, not accept touchs on subviews:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    if (touch.view != _mySuperView) { // accept only touchs on superview, not accept touchs on subviews
        return NO;
    }

    return YES;
}
vietstone
  • 8,784
  • 16
  • 52
  • 79
15

iOS 6 introduces a great new feature that solves this exact problem - a UIView (subview) can return NO from gestureRecognizerShouldBegin: (gesture recognizer attached to a superview). Indeed, that is the default for some UIView subclasses with regard to some gesture recognizers already (e.g. a UIButton with regard to a UITapGestureRecognizer attached to a superview).

See my book on this topic: http://www.apeth.com/iOSBook/ch18.html#_gesture_recognizers

matt
  • 515,959
  • 87
  • 875
  • 1,141
13

I managed to get it working by doing the following:

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureHandler:)];

// ...

-(void) tapGestureHandler:(UITapGestureRecognizer *)sender {
    CGPoint point = [sender locationInView:sender.view];
    UIView *viewTouched = [sender.view hitTest:point withEvent:nil];
    if ([viewTouched isKindOfClass:[ThingIDontWantTouched class]]) {
        // Do nothing;
    } else {
        // respond to touch action
    }
}
Jian Jie
  • 289
  • 1
  • 2
  • 13
Awais Hussain
  • 1,501
  • 1
  • 14
  • 15
6

Remember to add and set the delegate method -

In your UIViewController's .h file include this delegate

@interface YourViewController: UIViewController<UIGestureRecogniserDelegate>

Then where you're creating your tapGesture object, (within viewDidLoad for example)

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(actionMethodYouWantToHappenOnTap)];  

    tap.delegate = self;  //Remember to set delegate! Otherwise the delegate method won't get called.

    [self.view addGestureRecognizer:tap];

Remembering to set the delegate method tap.delegate = self; then the delegate method for tap will now fire on tap.

Within this method you handle when you would like the tap gesture recognition to be used, in my code I wanted it to ignore a tap if a particular subview was visible

-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{

     if (!mySubview.hidden){
     return NO;   //This fired if said subview was visible, though whatever the condition of where and when you want tap to work, can be handled within this delegate method.
     }

    return YES;
}

I hope this helps anyone else with the same issue. Remembering to set the delegate method I found was something that's easily overlooked.

Cheers

Jim Tierney
  • 4,078
  • 3
  • 27
  • 48
3

Lots of answers, adding another alternate solution. Initialise the view tag of the subviews you don't want to receive touches from as 1 and do the following:

- (void)viewDidLoad {
    [super viewDidLoad];
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    tap.delegate = self;
    [self.view addGestureRecognizer:tap];
}

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    if (touch.view.tag == 1) {
        return NO;
    }
    return YES;
}

-(void)handleTap:(id)sender
{
//Handle tap...
}

This way, the touch gesture will be received only from the views you require it from.

Koushik Ravikumar
  • 664
  • 11
  • 26
1

updated to Swift 5

@Awais Hussain's answer is perfect for that,

view with gesture -> Controller.view -> subView -> ViewThingI_DontWantToBeTouched

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        let tap = UITapGestureRecognizer(target: self, action: #selector(ViewController.tapGestureHandler(_:)))

    }


    @objc
    func tapGestureHandler(_ sender: UITapGestureRecognizer){
        let point = sender.location(in: sender.view)
        if let viewTouched = sender.view?.hitTest(point, with: nil), viewTouched is ViewThingI_DontWantToBeTouched{
            ()
            // Do nothing;
        }
        else {
            // respond to touch action
        }
    }
}
black_pearl
  • 2,549
  • 1
  • 23
  • 36
0

If you added UITapGestureRecognizer for ContainerView, it should respond with all over ContainerView. It will not mind its subviews.

Check the Gesture location, if its in your subview position,just skip the gesture actions.

- (void) tapGestureHandler:(UIGestureRecognizer *) gestureRecognizer {


    CGPoint tapPoint = [gestureRecognizer locationInView:nil]; 

   //check your tapPoint contains ur subview frame, skip.else do ur actions
}
Shamsudheen TK
  • 30,739
  • 9
  • 69
  • 102
0

ADDED:

i just thought of something better.

in your handler

-(void)tapGestureHandler:(UITapGestureHandler)gesture

check if gesture.view is the superview. it will return the subviews if subviews are tapped.

====================================================

I would suggest overriding within the superview.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

this is called to determine if a gesture falls within a view

see the answer to this question to see how it behaves by default.

Event handling for iOS - how hitTest:withEvent: and pointInside:withEvent: are related?

you can check if the point is in any of its subviews. if yes, return nil, else return self.

OR

in each of the subviews, add a tapgesture recognizer that does nothing. the gesture recognizer of a subview will cancel the gesture recognizer of its superview by default. i would keep an eye on the memory footprint if there are many subviews though.

Community
  • 1
  • 1
tzl
  • 1,540
  • 2
  • 20
  • 31
  • Unfortunately I don't think this works. According to the docs, gesture.view gives you "The view the gesture recognizer is attached to." which gives the entire container view and not the subview. The second option looks promising, I'll give it a go. – Awais Hussain Mar 18 '13 at 03:19
  • Great, I got it working by using the hitTest:withEvent method. See post below. – Awais Hussain Mar 18 '13 at 03:46
0

Updated Answer for Swift 4

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        if touch.view != yourView
        {
            return false
        }

        return true
    }

Before that make sure you have setting delegate property to your object ..

abdul sathar
  • 2,395
  • 2
  • 28
  • 38