Friday, October 3, 2008

A Bit About the Responder Chain

Yesterday's post about handling double-taps provoked a question about how to handle double-taps using a UIWebView. I haven't worked much with UIWebView, but I think a little explanation of the way the responder chain works on the iPhone may help; it's a little different than the responder chain on Mac OS X.

Controllers are now officially part of the responder chain. Any event goes first to the First Responder, as you might expect.

If the First Responder (which is usually the UI element being interacted with) doesn't consume the event, it is not always passed directly up to the superview, as it is on OS X. Rather the First Responder checks to see if it has a controller, and if there is one, it does a lateral pass over to it. This gives the controller class the opportunity to respond to and/or consume events before it is passed up to the superview. This is why you can implement methods like touchesBegan:withEvent: on either a view or a view controller.

This gives you two options if you need to intercept events normally handled by a UIKit object, like the UIView: You can implement the touchesBegan:withEvent: either on the object's controller, or you can create a subclass of the object and implement the method there.

I don't know which should be used with UIWebView because I don't know if UIView consumes the double tap event. I'm guessing it does (I'll update if I get confirmation of this), so my guess is that you would have to subclass UIWebView to intercept double-taps. If it doesn't consume the event, then you could just intercept it in the controller class for the web view.

Whoah - made a little mistake in the original posting. When implementing this code in a controller class, you want to forward to the next responder. When subclassing an existing view that handles touches, you want to pass to super! I've corrected the post below to reflect the difference


Here's the trick, though. Implementing a method like touchesBegan:withEvent, by default, consumes that event so that it goes no further through the Responder Chain. However, if you're intercepting an event for a control that detects touches or gestures, then you need to make sure that you pass the event down the responder chain manually if you don't handle it completely.

Here is how you might intercept a double-tap in your controller class, letting any events that are not handled by your method proceed down th responder chain:

-(void)respondToFictionalEvent:(UIEvent *)event {
if (someCondition)
[self handleEvent:event];
else
[self.nextResponder respondToFictionalEvent:event];
}


When subclassing an existing view than has touch-handling code, you have to handle it a little differently. What you have to do instead, is to pass the unhandled event up to super, like so:


-(void)respondToFictionalEvent:(UIEvent *)event {
if (someCondition)
[self handleEvent:event];
else
[super respondToFictionalEvent:event];
}


Basically, if you don't want to consume the event, you have to manually call the same method on the next responder. That's it. That's the magic that lets you intercept some, but not all, events for an existing object. So, to intercept a double-tap in a subclass of UIWebView, you might do something like this:

Header: MyUIViewSubclass.h

#import <UIKit/UIKit.h>


@interface MyUIViewSubclass : UIView {

}

@end


Implementation: MyUIViewSubclass.m

#import "MyUIViewSubclass.h"


@implementation MyUIViewSubclass

- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event {


UITouch *touch = [touches anyObject];
NSUInteger tapCount = [touch tapCount];
BOOL consumed = NO;

if (tapCount == 2) {
// Do whatever...
consumed = YES;
}



if (!consumed)
[super touchesBegan:touches withEvent:event];
}

@end


Note - this is not tested code, but it should give you an idea of how the process works. This is the basic process you would use any time you need to intercept some, but not all, responder chain events.



8 comments:

Brent Snyder said...

Jeff,
I am not clear on this and wondered if you could elaborate. I have subclassed a UITextView to correctly handle a custom double tap by overriding the touchesBegan message. All of the standard touch handling still works correctly such as scrolling down on the content. I would now like to handle a swipe and let the control handle everything else. however when I override touchesMoved the control no longer handles any of its standard scrolling no matter if I am putting in your [self.next repsonder] below is the code.

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = touches.anyObject;
CGPoint currentTouchPosition = [touch locationInView:self];
//[self.nextResponder touchesMoved:touches withEvent:event];
// If the swipe tracks correctly.
if (fabsf(startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN &&
fabsf(startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX)
{
// It appears to be a swipe.
NSLog(@"swipe");
if (startTouchPosition.x < currentTouchPosition.x)
{
//Right Swipe;
}
else
{
//Left Swipe;

}
} else {
[self.nextResponder touchesMoved:touches withEvent:event];
}
}

Jeff LaMarche said...

Hmm... I think I made a goof in my posting. I'll try and find some time to correct it. The code example I posted works correctly in a controller class, but not when subclassing (that's what I get for not testing my code before posting, huh?)

In the case of subclassing an existing object, what you need to do is to pass the unhandled event up to super rather than to nextResponder.

I apologize for that. You should still pass to nextResponder if implementing code like this in your controller class, or if you're completely overriding the touch behavior.

Barney Mattox said...

There is something I have not been able to understand and this is the closest article I have located on the subject.

If one "layers" UIViews, as presented in the apple documents, the top most view seems to block all of the others, while the apple docs seem to indicate the messages flow through from top to bottom (visually) until handled.

You provide examples for the code hierarchy (super, controller) but how do I pass messages down through the visual hierarchy. And in so doing how do I also facilitate controller or super correctly?

Barney

waspfish2000 said...

Barney, to answer your question, the app basically does a hitTest:withEvent: recursively on the views in the view hierarchy to determine which subview should be the first recipient of the event.

Nature Nut said...

Thanks for the info. I had a similar problem subclassing UIImageView. I wasn't receiving any calls to touchesBegan in my UIImageView subclass. I had to call [self setUserInteractionEnabled:TRUE]; in the init part of my subclass to enable the touch events to be passed to my subclass.
I thought this might help someone out there.

BMan said...
This comment has been removed by the author.
Vaibhav Tekam said...

Can you do the similar kind of thing, in iPhone Unity??

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels