Reputation: 1154
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:
Upvotes: 1
Views: 4608
Reputation: 181
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.
Upvotes: 4