Hacking the responder chain

This post could have also been called “How to allow user interaction with views beneath other views,” but that title is a little too long. I’m going to assume you have at least some idea of what the responder chain is for this post. (There’s an example xcode project below for those who like to skip straight to the nitty gritty.)

The Problem

It’s easy for a top-level view to get all the touch gestures (taps, drags, pinches, etc.) it needs, because the system knows to send all UI info to top-level views first. By “top-level,” I mean leaves (leafs?) of the subview tree of your one UIWindow object, which is usually called window as an instance variable in your app delegate.

It’s also easy to respond to touches if your view is a superview of a top-level view receiving touches. For example, suppose you built a custom view – let’s call it DragView – that responds to dragging, and this view contains a button as a subview. If the button is disabled and the user touches the button, the touch is still noticed by the underlying DragView.

This is all introduction for the hard case: What if you have a view underneath another view but not a superview of it? For example, the button in this subview tree is underneath the scrollView but not a superview of it:

In any case like this, by default, your “underneath” view will not get any touch information, because it will never be in the responder chain.

A Tempting Nonsolution

One idea is to forward events to your control. By this I mean that we override the touchesBegan:withEvent: method (and its friends touchesMoved: etc) in some object in the responder chain, and use that to send the event to the underneath view; something like this:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  [underneathView touchesBegan:touches withEvent:event];
}

But this is not the best approach for a few reasons, mainly:

  1. By the time touchesBegan: and friends get called, the touches have been filtered and bound to only include touches within the leaf subview.  If there were multiple touches, some outside that view, they would be split into different touchesBegan: (etc) calls, which can make things much less efficient or impossible depending on your exact view layout and complexity of touch actions.
  2. The standard UIKit controls ignore all forwarded touches.  They know it was forwarded by checking the view property of the UITouch objects sent in, which is set (aka “bound”) to the bottom-most leaf subview containing that touch before the call to touchesBegan and friends.

In summary, event forwarding can drop touches and simply won’t work for the built-in controls.  This approach can be useful for custom subviews, but I’m going to give a generally better solution here.

A Good Solution

The way to handle this is to inject code into the event-handling pipeline before touchesBegan (etc) is called.  You can achieve this by overriding hitTest:withEvent: on any common superview.  In the figure above, this object could be topView.

Modify that method to return the underneath view for the touch events you want it to handle instead of the normal leaf subview.  For example, your new hitTest: method might look like this:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  UIView *result = [super hitTest:point withEvent:event];
  CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
  if ([underButton pointInside:buttonPoint withEvent:event]) {
    return underButton;
  }
  return result;
}

This solves the above problems by taking control before the event is filtered and bound to another view.  It will end up being bound to whichever view you return.  Note that your hitTest: method will only be called for touches that occur in the view of that method.

Sample Code

HitTestDemo.xcodeproj (zipped)

References

Apple’s iPhone responder chain docs

UIView hitTest:withEvent: method docs

UIResponder touchesBegin:withEvent: and friends docs

UITouch class reference

UIEvent class reference

9 Comments