Question

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];
Was it helpful?

Solution

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];

OTHER TIPS

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:

  1. 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.

  2. 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.

  3. 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 UIViews 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

For this, when your view is tapped on which you have to delay the touch, you can start a timer with desired time and once time completes you can call the desired method you need.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top