UITapGestureRecognizer - make it work on touch down, not touch up?
Asked Answered
B

6

93

What I'm using the tap event for is very time-sensitive, so I'm curious if it's possible to make UITapGestureRecognizer activate when the user simply touches down, rather than requiring them to touch up as well?

Bulgarian answered 26/3, 2013 at 2:10 Comment(1)
If it helps, UITouch has a touchesStarted method. But that isn't using gesture recognizers, as you asked.Homomorphism
H
143

Create your custom TouchDownGestureRecognizer subclass and implement gesture in touchesBegan:

TouchDownGestureRecognizer.h

#import <UIKit/UIKit.h>

@interface TouchDownGestureRecognizer : UIGestureRecognizer

@end

TouchDownGestureRecognizer.m

#import "TouchDownGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>

@implementation TouchDownGestureRecognizer
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    if (self.state == UIGestureRecognizerStatePossible) {
        self.state = UIGestureRecognizerStateRecognized;
    }
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    self.state = UIGestureRecognizerStateFailed;
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    self.state = UIGestureRecognizerStateFailed;
}


@end

implementation:

#import "TouchDownGestureRecognizer.h"
    TouchDownGestureRecognizer *touchDown = [[TouchDownGestureRecognizer alloc] initWithTarget:self action:@selector(handleTouchDown:)];
    [yourView addGestureRecognizer:touchDown];

-(void)handleTouchDown:(TouchDownGestureRecognizer *)touchDown{
    NSLog(@"Down");
}

Swift implementation:

import UIKit
import UIKit.UIGestureRecognizerSubclass

class TouchDownGestureRecognizer: UIGestureRecognizer
{
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent)
    {
        if self.state == .Possible
        {
            self.state = .Recognized
        }
    }

    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent)
    {
        self.state = .Failed
    }

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent)
    {
        self.state = .Failed
    }
}

Here is the Swift syntax for 2017 to paste:

import UIKit.UIGestureRecognizerSubclass

class SingleTouchDownGestureRecognizer: UIGestureRecognizer {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        if self.state == .possible {
            self.state = .recognized
        }
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .failed
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .failed
    }
}

Note that this is a drop-in replacement for UITap. So in code like...

func add(tap v:UIView, _ action:Selector) {
    let t = UITapGestureRecognizer(target: self, action: action)
    v.addGestureRecognizer(t)
}

you can safely swap to....

func add(hairtriggerTap v:UIView, _ action:Selector) {
    let t = SingleTouchDownGestureRecognizer(target: self, action: action)
    v.addGestureRecognizer(t)
}

Testing shows it will not be called more than once. It works as a drop-in replacement; you can just swap between the two calls.

Hols answered 26/3, 2013 at 4:9 Comment(14)
extra +1 for the "UIGestureRecognizerSubclass.h" import. Nice.Acaleph
Shouldn't super be invoked in the touchesBegin,touchesMoved,touchesEnded methods?Jewett
@JasonSilberman you forget #import <UIKit/UIGestureRecognizerSubclass.h>Hols
How would you implement double tap with this?Homicidal
Wait a minute, but how do you get Up and Down callbacks for the same tap? I want to know both the tap down and tap up events for the same tap. Does this just replace Up with Down?Blitzkrieg
@Blitzkrieg you have assign state to begin and end. Then check it's state in handle. Custom recognizer mean you can control everything.Hols
Thanks @Bum, I've been playing with that and I've noticed that while begin is always fired, end is fickle and not dependable - sometimes will fire and sometimes will NOT fireBlitzkrieg
@swipekeys.com hi, I found that this subclass will block my long press gesture recogniser, that never called after I add this recogniser to my code. I have one thought about it. I work on Swift, and there is no Recognized state, so I used Ended state. I wonder, could this be the case? Can you show code for Swift? Thanks.Jumble
Found the answer! Forget to added UIGestureRecognizerDelegate protocol and set delegate = self. Method - gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: is what you looking forJumble
The -state property is not writeable. Please consider updating the code above.Freelance
@Freelance you forget import <UIKit/UIGestureRecognizerSubclass.h>Hols
touches ended not called. Do u know why ?Beefwitted
bjour @LESANG, thanks for the great answer. I did paste in the latest Swift3 code. Feel free to edit, unwind or use the edit. Thanks againMissie
@LESANG this answer works but I lost the ability to swipe because as soon as I put my finger on the object to swipe it it thinks it's a touch. Why can't it differentiate between them both?Quicksand
A
203

Use a UILongPressGestureRecognizer and set its minimumPressDuration to 0. It will act like a touch down during the UIGestureRecognizerStateBegan state.

For Swift 4+

func setupTap() {
    let touchDown = UILongPressGestureRecognizer(target:self, action: #selector(didTouchDown))
    touchDown.minimumPressDuration = 0
    view.addGestureRecognizer(touchDown)
}

@objc func didTouchDown(gesture: UILongPressGestureRecognizer) {
    if gesture.state == .began {
        doSomething()
    }
}

For Objective-C

-(void)setupLongPress
{
   self.longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(didLongPress:)];
   self.longPress.minimumPressDuration = 0;
   [self.view addGestureRecognizer:self.longPress];
}

-(void)didLongPress:(UILongPressGestureRecognizer *)gesture
{
   if (gesture.state == UIGestureRecognizerStateBegan){
      [self doSomething];
   }
}
Accustomed answered 5/8, 2013 at 15:23 Comment(11)
for folks that like interface builder (me), minimumPressDuration can also be set in IB (thank you Rob for great solution)Eldridgeeldritch
Is there a way to detect touch up here?Gillum
@Gillum Yes, check for the gestures state UIGestureRecognizerStateEndedAccustomed
A nice workaround +1. minimumPressDuration can be 0 tho.Clyve
@ValentinRadu Ah changed it to zero tyAccustomed
remeber setting pressduration to 0 cancels touches in viewDictator
@Anish웃 Setting gestureRecognizer.cancelsTouchesInView = false will allow for other GR's to act on the event as well.Brotherton
DANGER it can often be called more than once, with a typical touch.Missie
Brilliant answerBuchheim
If it's called more than once be sure to add the if statement in the didTouchDown functionMultivibrator
This also helps mask the touch delay in a UITableView due to its UIScrollView (see delaysContentTouches).Edlyn
H
143

Create your custom TouchDownGestureRecognizer subclass and implement gesture in touchesBegan:

TouchDownGestureRecognizer.h

#import <UIKit/UIKit.h>

@interface TouchDownGestureRecognizer : UIGestureRecognizer

@end

TouchDownGestureRecognizer.m

#import "TouchDownGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>

@implementation TouchDownGestureRecognizer
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    if (self.state == UIGestureRecognizerStatePossible) {
        self.state = UIGestureRecognizerStateRecognized;
    }
}

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    self.state = UIGestureRecognizerStateFailed;
}

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    self.state = UIGestureRecognizerStateFailed;
}


@end

implementation:

#import "TouchDownGestureRecognizer.h"
    TouchDownGestureRecognizer *touchDown = [[TouchDownGestureRecognizer alloc] initWithTarget:self action:@selector(handleTouchDown:)];
    [yourView addGestureRecognizer:touchDown];

-(void)handleTouchDown:(TouchDownGestureRecognizer *)touchDown{
    NSLog(@"Down");
}

Swift implementation:

import UIKit
import UIKit.UIGestureRecognizerSubclass

class TouchDownGestureRecognizer: UIGestureRecognizer
{
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent)
    {
        if self.state == .Possible
        {
            self.state = .Recognized
        }
    }

    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent)
    {
        self.state = .Failed
    }

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent)
    {
        self.state = .Failed
    }
}

Here is the Swift syntax for 2017 to paste:

import UIKit.UIGestureRecognizerSubclass

class SingleTouchDownGestureRecognizer: UIGestureRecognizer {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        if self.state == .possible {
            self.state = .recognized
        }
    }
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .failed
    }
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        self.state = .failed
    }
}

Note that this is a drop-in replacement for UITap. So in code like...

func add(tap v:UIView, _ action:Selector) {
    let t = UITapGestureRecognizer(target: self, action: action)
    v.addGestureRecognizer(t)
}

you can safely swap to....

func add(hairtriggerTap v:UIView, _ action:Selector) {
    let t = SingleTouchDownGestureRecognizer(target: self, action: action)
    v.addGestureRecognizer(t)
}

Testing shows it will not be called more than once. It works as a drop-in replacement; you can just swap between the two calls.

Hols answered 26/3, 2013 at 4:9 Comment(14)
extra +1 for the "UIGestureRecognizerSubclass.h" import. Nice.Acaleph
Shouldn't super be invoked in the touchesBegin,touchesMoved,touchesEnded methods?Jewett
@JasonSilberman you forget #import <UIKit/UIGestureRecognizerSubclass.h>Hols
How would you implement double tap with this?Homicidal
Wait a minute, but how do you get Up and Down callbacks for the same tap? I want to know both the tap down and tap up events for the same tap. Does this just replace Up with Down?Blitzkrieg
@Blitzkrieg you have assign state to begin and end. Then check it's state in handle. Custom recognizer mean you can control everything.Hols
Thanks @Bum, I've been playing with that and I've noticed that while begin is always fired, end is fickle and not dependable - sometimes will fire and sometimes will NOT fireBlitzkrieg
@swipekeys.com hi, I found that this subclass will block my long press gesture recogniser, that never called after I add this recogniser to my code. I have one thought about it. I work on Swift, and there is no Recognized state, so I used Ended state. I wonder, could this be the case? Can you show code for Swift? Thanks.Jumble
Found the answer! Forget to added UIGestureRecognizerDelegate protocol and set delegate = self. Method - gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: is what you looking forJumble
The -state property is not writeable. Please consider updating the code above.Freelance
@Freelance you forget import <UIKit/UIGestureRecognizerSubclass.h>Hols
touches ended not called. Do u know why ?Beefwitted
bjour @LESANG, thanks for the great answer. I did paste in the latest Swift3 code. Feel free to edit, unwind or use the edit. Thanks againMissie
@LESANG this answer works but I lost the ability to swipe because as soon as I put my finger on the object to swipe it it thinks it's a touch. Why can't it differentiate between them both?Quicksand
R
26

Swift (without subclassing)

Here is a Swift version similar to Rob Caraway's Objective-C answer.

The idea is to use a long press gesture recognizer with the minimumPressDuration set to zero rather than using a tap gesture recognizer. This is because the long press gesture recognizer reports touch began events while the tap gesture does not.

import UIKit
class ViewController: UIViewController {

    @IBOutlet weak var myView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Add "long" press gesture recognizer
        let tap = UILongPressGestureRecognizer(target: self, action: #selector(tapHandler))
        tap.minimumPressDuration = 0
        myView.addGestureRecognizer(tap)
    }

    // called by gesture recognizer
    @objc func tapHandler(gesture: UITapGestureRecognizer) {

        // handle touch down and touch up events separately
        if gesture.state == .began {
            // do something...
            print("tap down")
        } else if gesture.state == .ended { // optional for touch up event catching
            // do something else...
            print("tap up")
        }
    }
}
Roughish answered 1/7, 2016 at 10:55 Comment(6)
@richy, It felt a bit like a hack to me, too, since the name is long press gesture recognizer, but this method is a lot easier than subclassing the view and like you said, it works great.Roughish
This solution cause scrolling issue in UIscrollView subclassesInquiline
I think bater approach is Custom Gesture recognizerInquiline
@Roughish I ran into the same issue with this that I ran into with lessing's answer. This answer works but I lost the ability to swipe because as soon as I put my finger on the object to swipe it it thinks it's a touch. How can it differentiate between them both?Quicksand
@LanceSamaria, If you need more complex touch recognition than just tap I would use a custom gesture recognizer.Roughish
@Roughish thanks for getting back to me. I’ll ask a new question regarding it. Cheers!Quicksand
H
1

This is another solution. Create subclass of UIControl. You can use it like UIView even in Storyboard because UIControl is subclass of UIView.

class TouchHandlingView: UIControl {
}

And addTarget to it:

@IBOutlet weak var mainView: TouchHandlingView!
...

mainView.addTarget(self, action: "startAction:", forControlEvents: .TouchDown)
...

Then the designated action will be called like UIButton:

func startAction(sender: AnyObject) {
    print("start")
}
Helvellyn answered 21/2, 2016 at 11:0 Comment(0)
Q
1

I needed the ability for my view to have a hair trigger so as soon as it's tapped it responds. Using both @LESANG answer worked and so did using @RobCaraway answer. The problem I ran into with both answers was I lost the ability to recognize swipes. I needed my view to rotate when swiped but as soon as my finger touched the view only the tap was recognized. The tapRecognizer was too sensitive and couldn't differentiate between a tap and a swipe.

This is what I came up with based off of @LESANG answer combined with this answer and this answer.

I put 6 comments in each event.

import UIKit.UIGestureRecognizerSubclass

class SingleTouchDownGestureRecognizer: UIGestureRecognizer {


    var wasSwiped = false

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {

        guard let view = self.view else { return }
        guard let touches = event.touches(for: view) else { return } // 1. compare that event in touchesBegan has touches for the view that is the same as the view to which your gesture recognizer was assigned

        if touches.first != nil {
            print("Finger touched!") // 2. this is when the user's finger first touches the view and is at locationA
            wasSwiped = false // 3. it would seem that I didn't have to set this to false because the property was already set to false but for some reason when I didn't add this it wasn't responding correctly. Basically set this to false
        }
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {

        guard let touch = touches.first else { return }

        let newLocation = touch.location(in: self.view)
        let previousLocation = touch.previousLocation(in: self.view)

        if (newLocation.x > previousLocation.x) || (newLocation.x < previousLocation.x) {
            print("finger touch went right or left") // 4. when the user's finger first touches it's at locationA. If the the user moves their finger to either the left or the right then the finger is no longer at locationA. That means it moved which means a swipe occurred so set the "wasSwiped" property to true

            wasSwiped = true // 5. set the property to true because the user moved their finger
        }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        print("finger is no longer touching.") // 6. the user has lifted their finger off of the view. If "wasSwiped" is true then ".fail" but if it wasn't swiped then ".recognize"

        if wasSwiped {
            self.state = .failed
        } else {
            self.state = .recognized
        }
    }
}

And to use it so that view that uses it gets the hair trigger response and left and right swipe gestures.:

let tapGesture = SingleTouchDownGestureRecognizer(target: self, action: #selector(viewWasTapped(_:)))
myView.addGestureRecognizer(tapGesture)

let rightGesture = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture(recognizer:)))
rightGesture.direction = .right
myView.addGestureRecognizer(rightGesture)

let leftGesture = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture(recognizer:)))
leftGesture.direction = .left
myView.addGestureRecognizer(leftGesture)
Quicksand answered 16/1, 2020 at 14:33 Comment(0)
O
0

An alternative approach: Add an transparent button inside the UIView and assign touchUp and touchDown actions accordingly.

Oops answered 3/6, 2023 at 4:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.