Thursday, October 2, 2008

Handling Double Taps

Alright, since the NDA is down, let's celebrate by posting some information, shall we?

One question that I see posted a lot by new iPhone developers is "how do I handle double-taps?" The problem is that a double tap calls touchesBegan:withEvent: twice. If you want to have a different method called for a single-tap then a double-tap (in other words, if there's a double-tap, you don't want the single-tap method to fire), how do you handle it?

The answer lies in first using performSelector:withObject:afterDelay: to call the single-tap method after a delay that's short enough not to be noticed by the user, but long enough that the second tap of the double-tap will arrive before it actually executes. Then when the double-tap comes into touchesBegan:withEvent:, you use a little-known NSObject method called cancelPreviousPerformRequestsWithTarget:selector:object: to cancel the previous single-tap method call before calling the double-tap method.

Heres a code snippet that shows how to handle up to a quadruple-tap, with each number of taps resulting in a different method getting called.


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
NSUInteger tapCount = [touch tapCount];

switch (tapCount) {
case 1:
[self performSelector:@selector(singleTapMethod) withObject:nil afterDelay:.4];
break;
case 2:
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTapMethod) object:nil];
[self performSelector:@selector(doubleTapMethod) withObject:nil afterDelay:.4];
break;
case 3:
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(doubleTapMethod) object:nil];
[self performSelector:@selector(tripleTapMethod) withObject:nil afterDelay:.4];
break;
case 4:
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(tripleTapMethod) object:nil];
[self quadrupleTap];
break;
default:
break;
}

}
@end

5 comments:

phil swenson said...

Good post. I'd like to see how to handle taps (one im my case) on a UIWebView..... I'm stumped on this.

Jeff LaMarche said...

Phil - I'll do a post on this...

cirineu said...

Perfect!!! I had this problem, all the time my double tap was triggering my single tap method! Thanks a lot!

iVinay said...

great that works fine!! solved my problem.. thanks

Rudif said...

Hi Jeff

I want to have a singleTap and a doubleTap action on a button. Investigating, I tried your logic in this post, as well as logic suggested in Apple doc.

On simulator, SDK 3.0 - in the case of double tap the button receives twice in a row the touchesBegan event with tap count of 2 - no idea why.

Your logic (as reinterpreted by me) produces 2 calls to -doubleTap while Apple logic produces just one call, as appropriate.

Do you have any comments on this?

Below is my code, from a simple view-based app.

Rudi

PS : what is the trick for preserving code indentation in posts?


// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[button1 setTitle:@"Button 1" forState:UIControlStateNormal];
[button1 sizeToFit];
button1.center = self.view.center;
[button1 addTarget:self action:@selector(action1:forEvent:) forControlEvents:UIControlEventAllEvents];
[self.view addSubview:button1];
}

- (void)action1:(id)sender forEvent:(UIEvent *)event {
NSSet *touches = [event allTouches];
UITouch* touch = [touches anyObject];
NSUInteger tapCount = [touch tapCount];
NSUInteger phase = [touch phase];
NSLog(@"%s : tapCount=%d phase=%d %@", __FUNCTION__, tapCount, phase, [self strPhase:phase]);
#ifdef BEFORE
// ref http://iphonedevelopment.blogspot.com/2008/10/handling-double-taps.html
if (phase == UITouchPhaseBegan) {
switch (tapCount) {
case 1:
[self performSelector:@selector(singleTapMethod) withObject:nil afterDelay:0.3];
break;
case 2:
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTapMethod) object:nil];
[self doubleTapMethod];
break;
default:
break;
}
}
// above sees 2 x : tapCount=2 phase=0 and so calls the doubleTapMethod twice
#else
// ref http://developer.apple.com/IPhone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/EventHandling/EventHandling.html#//apple_ref/doc/uid/TP40007072-CH9
if (phase == UITouchPhaseEnded && tapCount == 1) {
[self performSelector:@selector(singleTapMethod) withObject:nil afterDelay:0.3];
}
else if (phase == UITouchPhaseBegan && tapCount == 2) {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTapMethod) object:nil];
}
else if (phase == UITouchPhaseEnded && tapCount == 2) {
[self doubleTapMethod];
}
// above works
#endif
}

-(void) singleTapMethod {
NSLog(@"%s : ", __FUNCTION__);
}

-(void) doubleTapMethod {
NSLog(@"%s : ", __FUNCTION__);
}