4

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!

4bar
  • 531
  • 2
  • 14
  • Graphics representation of your thing will helpful, or demo project :), or few line of codes – Jageen Oct 03 '17 at 08:20
  • Make sure UIView that you "substituted" as root view of child controller has user interaction enabled set to true – Sandeep Bhandari Oct 03 '17 at 08:26
  • Thanks @SandeepBhandari, I did have user interaction enabled both when using a vanilla UIView and when using my subclass. I experimented with turning that off (thinking that might be what's needed to prevent touches from traveling up the chain), but that kills user interaction on all of its subviews as well (my scroll view, etc.). – 4bar Oct 03 '17 at 08:35
  • @4bar : Try implementing hitTest as I have posted in my answer :) That should help you out – Sandeep Bhandari Oct 03 '17 at 08:40
  • @4bar : Looks interesting. Will keep an eye on the question :) U cant simply return nil in hit test thats why I added the condition and calling super.hitTest but now that u say your scrollView covers the whole view and hit test will always succeeds am deleting my answer – Sandeep Bhandari Oct 03 '17 at 09:15
  • Here's an example. Let's say I pan left inside my scroll view (which only scrolls vertically). That touch is not recognized by the scroll view's internal pan gesture recognizer, so is ignored and passed up. But instead of being picked up by `touchesBegan` on its superview (the root view of the child), it seems to go straight to the parent's view's gesture recognizers. That's what I find confusing. I suppose I could just disable those recognizers as needed, but I really want to understand the problem. – 4bar Oct 03 '17 at 09:17
  • @SandeepBhandari thanks for taking a look, and if you think the question is interesting, please feel free to upvote it. :) – 4bar Oct 03 '17 at 19:10
  • Interesting issue. I tried myself overriding the `UIResponder.next` to return `nil`, but gesture recognizers seem to be prioritized somehow... Thanks for the solution below, but still would be nice to know whats the problem here... – Maciek Czarnik Feb 21 '21 at 18:30

1 Answers1

4

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.

4bar
  • 531
  • 2
  • 14
  • Thanks. I rearranged the view to be an ancestor of the "top" view and the ancestor now gets the unhandled gestures. Before it was only a child of an ancestor and did not get unhandled gestures. – William J Bagshaw Jul 07 '20 at 14:04
  • @4bar - you deserve a Noble for that answer! Brilliant example of thinking outside of the box! – Maciek Czarnik Feb 21 '21 at 18:18