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

  1. Jordan
    Posted August 23, 2010 at 2:02 pm | Permalink

    This is a very good post on hit test. Well done!

  2. Posted December 5, 2010 at 10:01 pm | Permalink

    Very helpful to understand hitTest etc.

  3. Android.kc
    Posted April 6, 2011 at 7:39 pm | Permalink

    Thank you very much for this great tutorial!

    I tried to use this trick. Instead of interact with a button, I want to interact with a UIWebview. but could not make it work. I could not click on the link on the webview. Could you please help? thanks again.

    Based on the sample code, below are my changes

    //===in UnderneathButtonView.m

    #import “UnderneathButtonView.h”

    @implementation UnderneathButtonView

    @synthesize webview;

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint hitPoint = [webview convertPoint:point fromView:self];
    if ([webview pointInside:hitPoint withEvent:event]) return webview;
    return [super hitTest:point withEvent:event];
    }
    @end

    //==== in MainViewController.m, I changed the button to a uiwebview

    UIWebView *wv = [[[UIWebView alloc] initWithFrame:CGRectMake(0, 0, self.view. frameWidth, (int)(self.view.frameHeight / 4))] autorelease];
    wv.backgroundColor = [UIColor redColor];
    NSString *preloadUrl = @”http://www.google.ca”;
    NSURL *url = [NSURL URLWithString:preloadUrl];
    NSURLRequest *requestObj = [NSURLRequest requestWithURL:url ] ;
    [wv loadRequest:requestObj];
    [topView addSubview:wv];

    topView.webview = wv;

  4. Posted April 7, 2011 at 11:07 pm | Permalink

    Hi @Android, I can’t figure out your problem based on your post, but here are some thoughts that might help out:

    * If you want a button that can always be pressed, and overlaps your web view, you can just make it a sibling of the UIWebView. Often simpler is better, and this is a simple solution. You could set up the view like this:

    [myView addSubview:webView];
    [myView addSubview:myButton]; // Important that it’s called second!

    Views added later are “on top of” earlier views, analogous to applying paint to a canvas.

    * UIWebView does not play nicely with touches. If you think about it, when a user touches down in a web view, are they going to scroll or tap a button? There’s no way to know immediately, so instead of passing through the touch information, I think UIWebView holds on to it for a moment to wait for the user to scroll (or not). UIScrollView does the same thing. In summary, UIWebView may be behaving in ways that other views do not. Perhaps this is part of the problem? You could check by temporarily replacing your UIWebView with a UIButton to see if that fixes things.

    * If you really really want to use your custom implementation of hitTest:withEvent:, make sure it’s being called at all. You can add the line:
    NSLog(@”%s”, __FUNCTION__);
    which prints out the name of the current function. This is a very handy line to put in methods that aren’t working – always good to check if they’re being called at all! And you could use more NSLog’s, or add a breakpoint at that function and step through it to see what’s going on.

  5. David Hsu
    Posted May 13, 2011 at 9:42 am | Permalink

    Hi Tyler,

    I see your post used as an example by many online. Thx for putting something up on hittestwithevent. There aren’t too many posts on this online.

    I have a question about hittest which i posted on stackoverflow but no one can answer. I’ve seen it asked a few times actually but still no answer.

    I have a UIScrollView with no subviews. When I touch the scrollview, i see its hittestwithevent called 3 times. Why is it 3 times and not 1?

    Here is my post:

    http://stackoverflow.com/questions/5972956/uiview-hittestwithevent-comes-in-3s

    Any insight would be great.

    Thx,
    David

  6. Posted May 13, 2011 at 1:42 pm | Permalink

    Hi @David; I spent a few minutes investigating your question, but I couldn’t find a definitive answer. I did find some clues that might be helpful, which I’ll post in reply to your stackoverflow question. Let me know if you figure it out!

  7. Posted July 4, 2011 at 1:11 pm | Permalink

    Clear, concise, and clean. Thank you!

  8. Posted December 21, 2011 at 8:08 am | Permalink

    Thanks for your post! It was really helpful, I was able to solve a problem and learn from it a lot!

  9. Posted March 15, 2012 at 8:02 am | Permalink

    Great, thanks very much for a detailed post… The graphics are really helpful too.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*