4bar
4bar

Reputation: 561

Preventing unhandled touch events on a child view controller from passing through to container view

I have a container view controller managing its own full-screen content view, with several gesture recognizers attached. A child view controller can be overlaid over a portion of the screen; its root view is a UIView providing the opaque background color, which is covered by a UIScrollView, which in turn contains a complex view hierarchy of stack views, etc.

Scrolling in the child works correctly, as well as any user interactions with its subviews. The problem I'm having is that any taps or other non-scrolling gestures on the the scroll view itself (i.e. not inside any of its subviews) fall through the empty UIView behind it and are unexpectedly handled by the gesture recognizers on the root view of the parent (container) controller. I want those touches to be swallowed up by the child's background view so that they are ignored/cancelled.

My first thought was to override nextResponder on the child VC to return nil, assuming that would prevent touch events from passing to the superview. No success there, so I tried overriding the touch handling methods (touchesBegan: etc.) on the child controller, but they never get called. Then I substituted a simple UIView subclass to be the root view of my child controller, likewise trying both of those approaches there instead. Again returning nil for nextResponder has no effect, and the touch methods never get called.

My responder chain looks to be set up exactly as I would expect: scroll view --> child VC's root view --> child VC --> parent's root view --> parent VC. That makes me think my controller containment is set up correctly, and makes me suspect that the gesture recognizers on the parent's root view are somehow winning out over the responder chain in a way that I don't understand.

This seems like it should be easy. What am I missing? Thanks!

Upvotes: 5

Views: 2272

Answers (1)

4bar
4bar

Reputation: 561

I think I understand better what's going on here thanks to this very helpful WWDC video.

Given an incoming touch, first the system associates that touch with the deepest hit-tested view; in my case that's the UIScrollView. Then it apparently walks back up the hierarchy of superviews looking for any other attached recognizers. This behavior is implied by this key bit of documentation:

A gesture recognizer operates on touches hit-tested to a specific view and all of that view’s subviews.

The scroll view has its own internal pan recognizer(s), which either cancel unrecognized touches or possibly fall back on responder methods that don't happen to forward touches up the responder chain. That explains why my responder methods never get called, even when my own recognizers are disabled.

Armed with this information, I can think of a few possible ways to solve my problem, such as:

  • Use gesture delegate methods to ignore touches if/when the associated view is under a child controller.
  • Write a "null" gesture recognizer subclass that captures all touches and ignores them, and attach that to the root view of the child controller.

But what I ended up doing was simply to rearrange my view hierarchy with a new empty view at the top, so that my child controller views can be siblings of the main content view rather than its subviews.

So the view hierarchy changes from this:

"Before" hierarchy

to this:

"After" hierarchy

This solves my problem: my gesture recognizers no longer interact with touches that are hit-tested to the child controller's views. And I think it better captures the conceptual relationships between my app's controllers, without requiring any additional logic.

Upvotes: 4

Related Questions