14
Jun
2010
 

Differentiating Tap Counts on iOS [UPDATED]

by Matt Long

In your iPhone/iPad apps you often need to know how many times your user tapped in a view. This can be challenging because, though the user may have tapped twice, you will receive the event and it will look like they tapped once as well as twice. If the user triple-tapped, you will get the event for one tap, two taps, and three taps. It can get a little frustrating, but the trick is timing. You simply have to wait a period of time to see if another tap comes. If it does, you cancel the action spawned by the first tap. If it doesn’t you allow the action to run. There’s a few little nuances to getting it to work, but it can be done. Here is how.

Overriding Touch Event Handlers

In order to know how many times your user tapped, you listen for the events in the various touch handlers. Let start with -touchesEnded. This is where we will determine how many taps we received and respond accordingly. Consider the following code.

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event 
{
  if(touches.count == 1)
  {  
    if([[touches anyObject] tapCount] == 2)
    {
      [self handleDoubleTap];
    }
    else if([[touches anyObject] tapCount] == 3)
    {
      [self handleTripleTap];
    }
    else
    {
      [self handleSingleTap]; 
    }
  }
}

If you place a breakpoint inside each of the code blocks for the three tap counts, you will find that in the case where you triple tap, it will break in all three. If you double tap, it will break in both the double tap and single tap and of course if you single tap, it will break in the single tap branch.

In most cases, this is not desirable. You will likely want a clean differentiation between each touch as each tap count will likely mean something different. So how can we fix this? The trick is to use -peformSelector:withObject:afterDelay and then canceling the perform action if a new action occurs. Got it?

Wait For It…

If you call the method directly as we did in the sample code above, there is no way to delay when it runs nor is there a way to cancel it should another tap be received. Both of these things are necessary. If you think about it, waiting to run our -handleSingleTap method until a certain amount of time passes helps make sure it actually was a double tap. If the user taps once and nothing else occurs, we’re safe to run the -handleSingleTap code. If another tap is received in the mean time, however, we can cancel the action from the first tap. The way we do this is by changing our -touchesEnded: code to something like this:

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event 
{
  if(touches.count == 1)
  {  
    if([[touches anyObject] tapCount] == 2)
    {
      [self performSelector:@selector(handleDoubleTap)
                 withObject:nil
                 afterDelay:0.35]; 
    }
    else if([[touches anyObject] tapCount] == 3)
    {
      [self handleTripleTap];
    }
    else
    {
      [self performSelector:@selector(handleSingleTap)
                 withObject:nil
                 afterDelay:0.35]; 
    }
  }
}

Notice that we are delaying 350 milliseconds to see if another event occurs. You should also notice that we still call -handleTripleTap directly without using -performSelector:withObject:afterDelay. This is because it has now been isolated as a distinct event. If we get a triple tap, we’re pretty well assured now that a double or single tap event was not actually run as those events will have been cancelled. So how does that work? How do we cancel them?

Canceling The Action

Now that we are waiting around to see if another tap event occurs, we need to override the -touchesBegan: method. It will look something like this:

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
  [NSObject cancelPreviousPerformRequestsWithTarget:self
                                           selector:@selector(handleSingleTap)
                                             object:nil];

  [NSObject cancelPreviousPerformRequestsWithTarget:self
                                           selector:@selector(handleDoubleTap)
                                             object:nil];
}

The method -cancelPreviousPerformRequestsWithTarget:selector:object: is able to determine which action you want to cancel and then cancel it preventing the selectors, -handleSingleTap or -handleDoubleTap from being called–assuming the second or third taps occurred within the allocated 350 milliseconds. That’s all there is to it.

There *Is* a Catch

That is all there is to it, however, you have to be careful if you intend to pass parameters to your selectors. If you use a selector that takes a parameter so that you have to call something with -performSelector:withObject:afterDelay: passing an object to the second parameter, you will need that object or one identical to it in your call to cancel the action with -cancelPreviousPerformRequestsWithTarget:selector:object:. It must be able to evaluate to true when equals is called on the object passed into the object parameter. This can be tricky, but you can overcome it in one of two ways using an ivar:

– Create an ivar to hold onto the variable that you will use as a parameter to the cancel when you first pass it to the -performSelector:withObject:afterDelay: call.

– Create an ivar to hold onto the variable when you first enter touches ended and call -performSelector:withObject:afterDelay passing it nil for the object parameter. Then, grab the ivar when you need it in your handler code. The cancel call can then take nil for its object parameter.

These points are crucial if you are intending to pass parameters as the cancel will fail if you try to pass a parameter and it doesn’t match what you used in your call to -performSelector:withObjecct:afterDelay:. The only parameter that doesn’t matter is the afterDelay: param.

Conclusion

I’m finding the need to differentiate tap counts more and more often so this post is really as much a way for me to keep a journal of things I need to do frequently as it is to help others figure things out too. I hope it’s been helpful to you. Until next time.

Update

So, apparently Gesture Recognizers do address this issue. I had looked at them as a possible solution, but ran into the same differentiation problems, hence this blog post. However, Ashley Clark pointed me to the -requireGestureRecognizerToFail: method, which apparently enables you to have this cancellation functionality by creating a dependency between recognizers. The code to take advantage of it looks something like this:

UITapGestureRecognizer *tripleTap = 
[[UITapGestureRecognizer alloc]
 initWithTarget:self action:@selector(handleTripleTap:)];

[tripleTap setNumberOfTapsRequired:3];
[[self view] addGestureRecognizer:tripleTap];
[tripleTap release];

UITapGestureRecognizer *doubleTap = 
[[UITapGestureRecognizer alloc]
 initWithTarget:self action:@selector(handleDoubleTap:)];

[doubleTap setNumberOfTapsRequired:2];
[doubleTap requireGestureRecognizerToFail:tripleTap];
[[self view] addGestureRecognizer:doubleTap];
[doubleTap release];

UITapGestureRecognizer *singleTap = 
[[UITapGestureRecognizer alloc]
 initWithTarget:self action:@selector(handleSingleTap:)];

[singleTap setNumberOfTapsRequired:1]; // Unnecessary since it's the default
[singleTap requireGestureRecognizerToFail:doubleTap];
[[self view] addGestureRecognizer:singleTap];
[singleTap release];

So, if your project is 3.2 and later, use gesture recognizers. The effect is about the same, but the code is quite a bit cleaner.

Comments

cpryland says:

Gosh, shouldn’t you just assume 3.2 and use the gesture support?

Matt Long says:

@ cpryland

Gestures don’t fix the problem. Try creating two gesture recognizers–one that responds to two taps and one that responds to one. Then set a break point in them both. If you double tap, they both still get called.

Best regards.

aclark says:

Actually you can do this with UIGestureRecognizer. After creating your two (or more) recognizers simply instruct the single-tap recognizer that it can only succeed after the double-tap recognizer fails.

UITapGestureRecognizer *singleTap, *doubleTap;

singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleTap)];
doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap)];

doubleTap.numberOfTapsRequired = 2;

[singleTap requireGestureRecognizerToFail:doubleTap];

[view addGestureRecognizer:singleTap];
[view addGestureRecognizer:doubleTap];

In that way the singleTap recognizer will only have its action invoked if there is a delay between two taps longer than what the system has determined qualifies a double-tap. No magic numbers required.

Adion says:

I suppose you meant 350 milliseconds, since the source code says 0.35, which I would assume are seconds.

jamie314 says:

In the interest of picking nits:

The method -peformSelector:withObject:afterDelay: takes a delay in seconds, so 0.35 translates to 350 milliseconds, not 35.

It isn’t hugely relevant in this example, but depending on your application an intentional delay of 350ms may become noticeable to users.

PBenz says:

I tried this out last night and discovered that aclark’s suggestion of using gestures actually works, but Matt is also correct in that if you put a breakpoint in the handler for the single tap, it will get called. My test code simply NSLogs a line to the console, and the single tap handler definitely does not get called unless there’s a breakpoint set within it. I must admit that I don’t understand why.

Matt Long says:

@aclark

Nice! Had missed the requireGestureRecognizerToFail method. That’s great! I’ve got some re-factoring to do now, you big jerk. ;-)

Will update the blog post too.

-Matt

Matt Long says:

@jamie314, @Adion,

Right you are. 350 milliseconds. Fixed!

Thanks.

-Matt

yar2050 says:

Thank you for leaving this as the post + the update. The method without gesture recognizers is an important thing to have on file, too.