Ad

Our DNA is written in Swift
Jump

Double Tapping on Buttons

Grinarn asks:

“I got several buttons set up on my view and when the button gets clicked, a detailed view of that item appears.
What I need is another action method like double click or click and hold, to trigger another action.

How can I do this? I just found the events in the IB which seems only supports single touch events.”

Any view in the SDK can receive and process touch events. This gives you the ability to implement any kind of tap or gesture that you might dream up. But for everyday purposes we will find the methods provided by UIControl sufficient. UIControl inherits from UIView which means that it can do everything that views can do, but it adds the Target-Action mechanism.

For this mechanism you can attach a multitude of various events to each control by simply specifying a target (= any object instance), an action (= any selector of the target) and a constant from the following list. “Selector” is only fancy name for method signature, which consists of the method name and the names of the parameters, all with a colon behind them.

General Touch Actions

  • UIControlEventTouchDown
  • UIControlEventTouchDownRepeat
  • UIControlEventTouchDragInside
  • UIControlEventTouchDragOutside
  • UIControlEventTouchDragEnter
  • UIControlEventTouchDragExit
  • UIControlEventTouchUpInside
  • UIControlEventTouchUpOutside
  • UIControlEventTouchCancel

Specific to Editing Controls

  • UIControlEventValueChanged
  • UIControlEventEditingDidBegin
  • UIControlEventEditingChanged
  • UIControlEventEditingDidEnd
  • UIControlEventEditingDidEndOnExit

Generic Constants matching several Actions

  • UIControlEventAllTouchEvents
  • UIControlEventAllEditingEvents
  • UIControlEventApplicationReserved
  • UIControlEventSystemReserved
  • UIControlEventAllEvents

Now generally if you make a button then you would use the UIControlEventTouchUpInside event even though at first you might instinctively go for UIControlEventTouchDown. TouchUpInside is the standard as it allows the user to reconsider and move outside of the button before lifting his finger thus cancelling his action. Otherwise the button would be like a landmine where there is no way back after touching it.

Now there might be cases where you exactly WANT the action to be fired right when you touch the control. Then TouchDown is the right action. You also see a TouchDownRepeat action available, but this always comes in succession after a TouchDown. Therefore some additional trickery is necessary to be able to distinguish between single and double tapping a button.

The coolest way to do that that I have seen so far is by means of firing a delayed performSelector and canceling this if there is a TouchDownRepeat before it fires. For this example we set up a label and a button in a viewControllers viewDidLoad:

- (void)viewDidLoad
{
    [super viewDidLoad];
 
	// label is defined in header so that we can access it later
	label = [[UILabel alloc] initWithFrame:CGRectMake(80.0, 70.0, 120.0, 20.0)];
	[self.view addSubview:label];
 
	UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
	button.frame = CGRectMake(100.0, 100.0, 80.0, 50.0);
	[button setTitle:@"Tap Me!" forState:UIControlStateNormal];
 
	// register target-actions for single and repeated touch
	[button addTarget:self action:@selector(touchDown:) forControlEvents:UIControlEventTouchDown];
	[button addTarget:self action:@selector(touchDownRepeat:) forControlEvents:UIControlEventTouchDownRepeat];
 
	[self.view addSubview:button];
}
 
- (void)dealloc
{
	[label release];
	[super dealloc];
}

And somewhere at the end of the view controller we put this code:

#pragma mark Actions
- (void) singleTapOnButton:(id)sender
{
	label.text = @"Single Tap";
}
 
- (void) doubleTapOnButton:(id)sender
{
	label.text = @"Double Tap";
}
 
#pragma mark Button UIControl Actions
- (void) touchDown:(id)sender
{
	NSLog(@"Touch Down");
	// give it 0.2 sec for second touch
	[self performSelector:@selector(singleTapOnButton:) withObject:sender afterDelay:0.2];
}
 
- (void) touchDownRepeat:(id)sender
{
	[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTapOnButton:) object:sender];
	NSLog(@"Touch Down Repeat");
	[self doubleTapOnButton:sender];
}

Note the (id)sender parameter for actions which are used with this mechanism. Due the object-oriented nature of this all it’s actually the button or control itself sending the event. Or more precisely it calls these methods on the main thread and passes its self-reference as the first parameter. So you can always type cast the sender to the kind of class you expect to cause such an event and then access properties. One trick that I like to often to is to use the tag property of buttons so that I can have a single action for multiple buttons and decide which button it was for by looking at the tag.

The magic happens in the touchDown method. There you see that the method singleTapOnButton is put on delayed execution. Code execution does not pause here and the UI continues to be responsive until the time interval expires and the selector is called on the main thread. I found 0.2 seconds to be sufficient, but it can be any length you like. If there is only a single touch and not a repeat touch then that’s it, the selector is called and the label is set to show that we detected a single tap. If the user taps a second time within these 0.2 seconds then the previous delayed performSelectors are all cancelled and instead the doubleTapOnButton method is being called.

Now that was too easy, permit me to get your head spinning with the real Christmas Surprise I have in stock for you, freshly baked out of Dr. Touch’s Cocoa Kitchen. If you are using double taps all the time then you can also modify the sending behavior of UIButtons to always delay the calling of the method for TouchDown. Simple add the following header and implementation to your project.

UIButton+DoubleTapping.h

#import 
 
@interface UIButton (DoubleTapping)
 
@end

UIButton+DoubleTapping.m

#import "UIButton+DoubleTapping.h"
 
@implementation UIButton (DoubleTapping)
 
- (void) delayedSendAction:(NSDictionary *)parameters
{
	// unpack parameters
	NSString *actionString = [parameters objectForKey:@"action"];
	SEL action = NSSelectorFromString(actionString);
	id target = [parameters objectForKey:@"target"];
	UIEvent *event = [parameters objectForKey:@"event"];
 
	// now actually send the action
	[super sendAction:action to:target forEvent:event];
}
 
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
	// find out if this is a first tap
 
	NSString *actionString = NSStringFromSelector(action);
 
	NSString *touchDownMethodName = [[self actionsForTarget:target forControlEvent:UIControlEventTouchDown] lastObject];
	NSString *touchDownRepeatMethodName = [[self actionsForTarget:target forControlEvent:UIControlEventTouchDownRepeat] lastObject];
 
	// we assume that there is only one action registered for this control event type
	if ([touchDownMethodName isEqualToString:actionString])
	{
		// we delay first touches
 
		// package everything in dictionary so that we can pass it as single parameter
		NSDictionary *tmpDict = [NSDictionary dictionaryWithObjectsAndKeys:actionString, @"action",
								 target, @"target", event, @"event", nil];
 
		[self performSelector:@selector(delayedSendAction:) withObject:tmpDict afterDelay:0.2];
		return;
	}
	else if ([touchDownRepeatMethodName isEqualToString:actionString])
	{
		// Double Touch, we cancel the delayed request
		[NSObject cancelPreviousPerformRequestsWithTarget:self];
	}
 
	// all other events we simple pass on
	[super sendAction:action to:target forEvent:event];
}
@end

Now let this melt in your mouth for the full flavor. This makes use of the following special ingredients:

You can override SDK class methods by simple creating a category and then just putting in the methods you want to override. These changes are global, so you don’t even need to add this header to your code. It simply being there modifies the behavior of all UIButtons.

The sendAction method does not tell you the UIControlEvent constant it is being called for, but instead a UIEvent with lots of useless information. But we don’t mind, because we can get the name of the method that is specified as action by calling actionsForTarget:forControlEvent. Because we usually only have one action for a certain type it is sufficient to use lastObject for the array this returns.

For the sendAction we need 3 parameters, performSelector:withObject:afterDelay: only takes one. So we have to package it all in a dictionary. A selector parameter of type SEL cannot itself be put into this dictionary, but we can use NSStringFromSelector() to convert it into an NSString which will do just fine in there. To reverse this we use NSSelectorFromString().

Finally we only want to delayed calling of the TouchDown method be cancelled if we encounter a TouchDownRepeat. Otherwise the TouchUpInside would also cancel the TouchDown. That’s why we need to know the method name of the touch down method as well.

All this simplifies your code in the view controller.

#pragma mark Button UIControl Actions
- (void) touchDown:(id)sender
{
	label.text = @"Single Tap";
}
 
- (void) touchDownRepeat:(id)sender
{
	label.text = @"Double Tap";
}

Bear in mind that now you have to use TouchDown for single taps and TouchDownRepeat for double taps. Were you to link the single tap action to the TouchUpInside (as recommended) then this does not work, because you would get the TouchDownRepeat message immediately followed by a TouchUpInside. So you would have to maybe have a BOOL flag track if you have already reacted to the double touch and if YES then ignore the TouchUpInside.


Categories: Q&A

3 Comments »

Trackbacks

  1. Rob Pearson » Double Tapping on Buttons
  2. Making a Control @ Dr. Touch