Tap Gesture Recognizer attached with container view does not block touch event of button in container view but blocks toolbar button's touch event

StackOverflow https://stackoverflow.com/questions/21317106

Question

So I have a view controller which has a container view. The container view is embedded with a navigation controller which is also parent controller of a view controller. The storyboard is like this:

view controller(mainViewController) --> navigation controller --> view controller(contentViewController)

You can see screenshot of storyboard in the below.

The first arrow is a embed segue from container view to navigation controller. The second arrow is a relationship represents contentViewController is root view controller of the navigation controller.

mainViewController and contentViewController are objects of the same class, named testViewController. It is the subclass of UIViewController. Its implementation is simple. It only has three IBAction methods, nothing else. Here is the implementation code:

#import "TestViewController.h"

@implementation TestViewController

- (IBAction)buttonTapped:(id)sender {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil
                                                    message:@"button is tapped"
                                                   delegate:self
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
}

- (IBAction)barButtonTapped:(id)sender
{
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil
                                                    message:@"bar button is tapped"
                                                   delegate:self
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
}

- (IBAction)viewTapped:(id)sender {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil
                                                    message:@"view is tapped"
                                                   delegate:self
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles: nil];
    [alert show];
}        
@end

I added a Tap Gesture Recognizer to the container view in mainViewController. It sends viewTapped:(id)sender message to mainViewController when the container view is tapped. Inside of the root view of contentViewController, there is a button which sends buttonTapped:(id)sender message to contentViewController when tapped. And there is a bar button in the toolbar of contentViewController which sends barButtonTapped:(id)sender message to contentViewController when tapped. The initial scene is mainViewController. When the app is running, I found that only touch events of the bar button is blocked, touch event is handled correctly by the button. In Apple's documentation, Regulating the Delivery of Touches to Views, it says:

In the simple case, when a touch occurs, the touch object is passed from the UIApplication object to the UIWindow object. Then, the window first sends touches to any gesture recognizers attached the view where the touches occurred (or to that view’s superviews), before it passes the touch to the view object itself.

I thought touch event will not pass to the button. This really confused me. Can someone explain this behavior? Thank you very much.


Screenshot of the storyboard: the storyboard

Was it helpful?

Solution

The Event Handling Guide for iOS: Event Delivery: The Responder Chain's "The Responder Chain Follows a Specific Delivery Path" section describes how touch events are passed first to the view that was touched, then up through all of its superviews, then to the window, and finally to the application itself.

A simplified representation of your project's view hierarchy would be:

mainViewController's Root View
  | mainViewController's Container View (has Tap Gesture Recognizer)
  |   | UINavigationController's Root View
  |   |   | contentViewController's View
  |   |   |   | UIButton ("Button")
  |   |   | UINavigationController's Toolbar View
  |   |   |   | UIToolbarTextButton ("Item")

...so when you tap the button or the toolbar button, they receive the touch event before mainViewController's container view.

The reason why the button's event fires and the toolbar button's doesn't appears to be related to Event Handling Guide for iOS: Gesture Recognizers' "Interacting with Other User Interface Controls" section:

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.

That appears to explain why the UIButton is able to preempt the tap gesture recognizer, but it doesn't say anything explicit about the toolbar button.

If you print out the view hierarchy you'll find that the toolbar button is represented using a UIToolbarButton which is a private class that inherits directly from UIControl. Based on our observations we would assume that UIToolbarButton does not preempt gesture recognizers like the public UIControl subclasses do. When I swizzled its touchesCancelled:withEvent: method I found that it gets called after the tap gesture recognizer fires, which seems to be what you would expect based on Event Handling Guide for iOS: Gesture Recognizers's "Gesture Recognizers Get the First Opportunity to Recognize a Touch" section where they note:

...if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.

There are a few different ways you could modify this behavior and the one you picked would depend on your end goal. If you wanted to allow touches on the toolbar you could check if the UITouch sent to the gesture recognizer's delegate's gestureRecognizer:shouldReceiveTouch: was inside the toolbar's frame and return NO if it was. Blocking touches to the UIButton specifically would probably require subclassing, but if you wanted to block all touches to mainViewController's child view controllers you could add a transparent view over its container view.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top