iosobjective-cuikituitapgesturerecognizer

How to let a UIView handle a tap gesture then have the tap gesture passed down to another UIView lying beneath?


The documentation says set:

recognizer.cancelsTouchesInVew = NO;

and add the recognizer to the top view.

I tried and it does not work.

Below is the code pattern I used:

UIView *custom_overlay_1 = [[UIView alloc] initWithFrame : given_frame];
UIView *custom_overlay_2 = [[UIView alloc] initWithFrame : given_frame];

UITapGestureRecognizer *custom_recognizer_1, *custom_recognizer_2;

custom_recognizer_1 = [[UITapGestureRecognizer alloc]
                                     initWithTarget : self
                                     action         :@selector(handle_tap_1:)];

custom_recognizer_2 = [[UITapGestureRecognizer alloc]
                                     initWithTarget : self
                                     action         :@selector(handle_tap_2:)];

     
[custom_recognizer_1 setCancelsTouchesInView: NO];
[custom_recognizer_2 setCancelsTouchesInView: NO];

[custom_overlay_1 addGestureRecognizer: custom_recognizer_1];
[custom_overlay_2 addGestureRecognizer: custom_recognizer_2];

[self.view addSubview custom_overlay_2];
[self.view addSubview custom_overlay_1];

- (int) handle_tap_1 : (UITapGestureRecognizer *) gesture
{
   
    NSLog(@" *** *** handle_tap_1 !!!");
    
    if (gesture.state == UIGestureRecognizerStateRecognized)
        NSLog(@" *** *** recognized tap detected in handle_tap_1 !!!");
    
    return (0);    
}


- (int) handle_tap_2 : (UITapGestureRecognizer *) gesture
{
   
    NSLog(@" *** *** handle_tap_2 !!!");
    
    if (gesture.state == UIGestureRecognizerStateRecognized)
        NSLog(@" *** *** recognized tap detected in handle_tap_2 !!!");
    
    return (0);    
}

The problem is custom_overlay_1 is able to catch the tap gesture but custom_overlay_2 doesn’t.


Solution

  • To allow custom_overlay_2 's tap gesture to be regconized, you need to provide custom delegate to custom_recognizer_1 instead and overwrite method gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) (simply return true) instead as its docs specify:

    Return Value

    true to allow both gestureRecognizer and otherGestureRecognizer to recognize their gestures simultaneously. The default implementation returns false—no two gestures can be recognized simultaneously.

    Setting cancelsTouchesInView = false doesn't work because it just alter the way touch events be sent to UIView not to other gesture regconizers, you can read docs of UIGestureRegconizer for more detail:

    A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are canceled. The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties:

    @interface ViewController : UIViewController<UIGestureRecognizerDelegate>
    @end
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        UIView *custom_overlay_1 = [[UIView alloc] initWithFrame : CGRectMake(100, 200, 200, 200)];
        [custom_overlay_1 setBackgroundColor:UIColor.redColor];
        UIView *custom_overlay_2 = [[UIView alloc] initWithFrame : CGRectMake(0, 0, 100, 100)];
        [custom_overlay_2 setBackgroundColor:UIColor.greenColor];
        
        UITapGestureRecognizer *custom_recognizer_1, *custom_recognizer_2;
        
        custom_recognizer_1 = [[UITapGestureRecognizer alloc]
                               initWithTarget : self
                               action         :@selector(handle_tap_1:)];
        [custom_recognizer_1 setDelegate:self];
        
        custom_recognizer_2 = [[UITapGestureRecognizer alloc]
                               initWithTarget : self
                               action         :@selector(handle_tap_2:)];
        
        
        [custom_recognizer_1 setCancelsTouchesInView: NO];
        [custom_recognizer_2 setCancelsTouchesInView: NO];
        
        [custom_overlay_1 addGestureRecognizer: custom_recognizer_1];
        [custom_overlay_2 addGestureRecognizer: custom_recognizer_2];
        
        [self.view addSubview:custom_overlay_1];
        [custom_overlay_1 addSubview:custom_overlay_2];
    }
    
    - (int) handle_tap_1 : (UITapGestureRecognizer *) gesture
    {
        
        NSLog(@" *** *** handle_tap_1 !!!");
        
        if (gesture.state == UIGestureRecognizerStateRecognized)
            NSLog(@" *** *** recognized tap detected in handle_tap_1 !!!");
        
        return (0);
    }
    
    
    - (int) handle_tap_2 : (UITapGestureRecognizer *) gesture
    {
        
        NSLog(@" *** *** handle_tap_2 !!!");
        
        if (gesture.state == UIGestureRecognizerStateRecognized)
            NSLog(@" *** *** recognized tap detected in handle_tap_2 !!!");
        
        return (0);
    }
    
    //MARK: UIGestureRecognizerDelegate
    -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
        return YES;
    }
    @end
    

    Also note that if you add custom_overlay_1 and custom_overlay_2 to the same superview, their gesture recognizer can not both receive touch.

                       superview
                       /       \
           custom_overlay_1   custom_overlay_2
    

    For example, on UI custom_overlay_2 on top of custom_overlay_1 , you tap on custom_overlay_2 , it will be the hit-tested view, the responder chain will be:

    custom_overlay_2 -> superview
    

    Therefore custom_overlay_1 and its gesture regconizers can not receive touch events.

    To allow both custom_overlay_1 and custom_overlay_2 receive events, you need to make one become subview to another views like in my example code when custom_overlay_2 become subview of custom_overlay_1. Now responder chain will be ( when tap on custom_overlay_2):

    custom_overlay_2 -> custom_overlay_1 -> superview -> ViewControler...