In one of my view controllers I have several views that contain a UITapGestureRecognizer, along with an implementation of touchesBegan
. I need to prioritize the taps over touchesBegan
so I set the delaysTouchesBegan
property of the gesture recognizers to YES
. This works correctly, but there's one problem: the gesture recognizer delays touchesBegan
for too long. According to the documentation:
When the value of the property is YES, the window suspends delivery of touch objects in the UITouchPhaseBegan phase to the view. If the gesture recognizer subsequently recognizes its gesture, these touch objects are discarded. If the gesture recognizer, however, does not recognize its gesture, the window delivers these objects to the view in a touchesBegan:withEvent: message (and possibly a follow-up touchesMoved:withEvent: message to inform it of the touches’ current locations).
The problem basically is that when the gesture recognizer does not recognize the gesture and delivers these objects to touchesBegan
, that operation takes too long. Is there anyway to expedite it, or is it just that the processing of the gesture to determine whether it's a tap is intensive and is impossible to shorten?
Edit:
Here's some more info. This is the code I use to setup the gesture recognizer:
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tapRecognizer.cancelsTouchesInView = NO;
tapRecognizer.delaysTouchesBegan = YES;
tapRecognizer.delegate = self;
tapRecognizer.numberOfTapsRequired = 1;
tapRecognizer.numberOfTouchesRequired = 1;
[self.someView addGestureRecognizer:tapRecognizer];
I'd solve it with changing UITapGestureRecognizer
to UILongPressGestureRecognizer
. The title of those two is a bit misleading but with UILongPressGestureRecognizer
you can set the minimumPressDuration
:
The minimum period fingers must press on the view for the gesture to be recognized.
UILongPressGestureRecognizer *tapRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tapRecognizer.delegate = self;
tapRecognizer.minimumPressDuration = //Up to you;
[self.someView addGestureRecognizer:tapRecognizer];
Is there anyway to expedite it, or is it just that the processing of the gesture to determine whether it's a tap is intensive and is impossible to shorten?
The delay seems to be dependent on how long it takes to process the gesture, so yes, it is not possible to adjust it (you can log -touchesBegan:withEvent:
to see exactly when it is called). For example, if you touch a UIView
with a UITapGestureRecognizer
and don't move your finger, the tap recognizer still thinks there's a chance you'll lift your finger in that same position, which will be recognized as a tap. So it will keep waiting until you have panned your finger or lifted it. On the other hand if you pan immediately, the UIView gets sent -touchesBegan:withEvent:
with almost no delay.
Workarounds that you could try are:
Use the gesture recognizer's cancelTouchesInView
property instead. If you set this to YES
the UIView
will then process touches right away, and then if the gesture recognizer ends up recognizing the touches as a gesture, the view gets sent -touchesCancelled:withEvent:
, where you can roll back any changes you made.
Use cancelTouchesInView
with an NSTimer
in the UIView
. Start the NSTimer
in the UIView
's -touchesBegan:withEvent:
and when it fires, perform your action on the view. If the view gets sent -touchesCancelled:withEvent:
before the timer fires, cancel the timer.
Subclass UIGestureRecognizer
and implement your own tap recognizer :)
I've made an example of options 1 and 2. The example has a view which you can drag around the screen, and the view has a tap recognizer that changes the color of the view. If you tap the view but drag it a little before releasing, you'll see the 'roll back' snapping the view to its position before the touch. If you set the JCPanningView
's delayResponseToTouch
to YES
, you don't see any dragging within the delay period.
This may or may not work for you depending on how your UIView
s are handling their touches.
Here is the view controller's interface (JCGestureViewController.h
):
#import <UIKit/UIKit.h>
@interface JCGestureViewController : UIViewController
@end
@interface JCPanningView : UIView
@property (nonatomic) BOOL delayResponseToTouch;
- (void)changeColor;
@end
and implementation (JCGestureViewController.m
):
#import "JCGestureViewController.h"
@interface JCGestureViewController ()
@property (strong, nonatomic) JCPanningView *panningView;
@end
@implementation JCGestureViewController
- (void)viewDidLoad
{
[super viewDidLoad];
_panningView = [[JCPanningView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 200.0f, 200.0f)];
[self.view addSubview:_panningView];
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(tappedPanningView)];
tapRecognizer.cancelsTouchesInView = YES;
[_panningView addGestureRecognizer:tapRecognizer];
// set this to YES to have a timer delay the view's response to touches
_panningView.delayResponseToTouch = NO;
}
- (void)tappedPanningView
{
[self.panningView changeColor];
}
@end
@interface JCPanningView ()
@property (nonatomic) CGPoint touchBeganLocation;
@property (nonatomic) CGPoint centerWhenTouchBegan;
@property (nonatomic) BOOL respondToTouches;
@property (nonatomic) NSTimer *timer;
@end
@implementation JCPanningView
- (void)dealloc
{
if (_timer) {
[_timer invalidate];
_timer = nil;
}
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [self randomColor];
}
return self;
}
- (void)changeColor
{
self.backgroundColor = [self randomColor];
}
- (CGFloat)randomRGBValue
{
return (arc4random() % 255) / 255.0f;
}
- (UIColor *)randomColor
{
return [UIColor colorWithRed:[self randomRGBValue] green:[self randomRGBValue] blue:[self randomRGBValue] alpha:1.0f];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"touchesBegan:");
[super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
self.touchBeganLocation = [touch locationInView:self.superview];
self.centerWhenTouchBegan = self.center;
if (self.delayResponseToTouch) {
if (self.timer) {
[self.timer invalidate];
}
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(startRespondingToTouches)
userInfo:nil
repeats:NO];
}
}
- (void)startRespondingToTouches
{
self.respondToTouches = YES;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
self.center = self.centerWhenTouchBegan;
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
if (self.delayResponseToTouch) {
self.respondToTouches = NO;
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesMoved:touches withEvent:event];
if (self.delayResponseToTouch && !self.respondToTouches) {
return;
}
UITouch *touch = [touches anyObject];
CGPoint newLocation = [touch locationInView:self.superview];
CGPoint delta = CGPointMake(newLocation.x - self.touchBeganLocation.x, newLocation.y - self.touchBeganLocation.y);
self.center = CGPointMake(self.centerWhenTouchBegan.x + delta.x, self.centerWhenTouchBegan.y + delta.y);
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if (self.delayResponseToTouch) {
self.respondToTouches = NO;
}
}
@end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With