How to cancel LongPressGestureRecognizer? - ios

I have a view that has a LongPressGestureRecognizer assigned to it which calls the following method:
#IBAction func longPressOnView1Recognized(_ sender: UIGestureRecognizer) {
if sender.state == .began {
// this runs when user's finger is down a "long time"
}
if sender.state == .ended {
// this runs when user's finger goes up again after the .began state
}
}
This all works as expected, but I'm trying to find a (good/proper) way of being able to programmatically cancel the long press recognizer (in certain circumstances) while the user's finger is still down.
That is, while a user's finger is still down on the view, and the recognizer has entered the .began state, (but before the user has lifted their finger -- before recognizer enters the .ended state)... is there some code we can run that will prevent the method above from firing when the user lifts their finger... like prematurely telling IOS to no longer listen for UP events for the remainder of this gesture?
I've read these docs, but I don't have that much experience with IOS touch, and I can't seem to find any method that is designed for this purpose.
my GestureRecognizer.reset() does not seem to do what I'm describing.
I can think of two possibilities:
1) A boolean flag, that would go inside the if sender.state == .ended {} closure
2) this:
myLongPressRecognizer.isEnabled = false
myLongPressRecognizer.isEnabled = true
Both of these work but seem not so great.

You are all good with disabling and reenabling the gesture recognizer so doing
myLongPressRecognizer.isEnabled = false
myLongPressRecognizer.isEnabled = true
is completely correct.
What I am worried about is you don't completely understand gesture recognizers. You should always use switch statement when handling gesture recognizer. Check the comments:
func handleLongPressGestureRecognizer(_ sender: UIGestureRecognizer) {
switch sender.state {
case .began:
// This will be called only once when the gesture starts
print("Long press did begin at \(sender.location(in: sender.view))")
case .changed:
// This will be called whenever your finger moves (at some frequency obviously).
// At this point your long press gesture is acting exactly the same as pan gesture
print("Long press changed position to \(sender.location(in: sender.view))")
case .ended:
// This is when user lifts his finger assuming the gesture was not canceled
print("Long press ended at \(sender.location(in: sender.view))")
case .cancelled:
// This is equally important as .ended case. You gesture may be canceled for many reasons like a system gesture overriding it. Make sure to implement logic here as well.
print("Long press canceled at \(sender.location(in: sender.view))")
case .failed, .possible:
// These 2 have been added additionally at some point. Useless as far I am concerned.
break
}
}
So at least you should handle cancelled status. But also note that the changed status will be triggered whenever the gesture is moved.

You have your solution on hand already. Toggling the UILongPressGestureRecognizer isEnabled is the best way to go. Setting the state property isn't possible, because it's a get-only property.
open var state: UIGestureRecognizer.State { get } // the current state of the gesture recognizer
isEnabled property is documented as:
default is YES. disabled gesture recognizers will not receive touches. when changed to NO the gesture recognizer will be cancelled if it's currently recognizing a gesture.

You can import the gesture recognizer header:
import UIKit.UIGestureRecognizer
That will make the state property a readwrite property. Thus, to cancel the gesture, just change its state to .cancelled.
So, for example, you can cancel the long press gesture recognizer one second after it was recognized with something like:
weak var timer: Timer?
#objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
print("began")
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
gesture.state = .cancelled
}
case .ended, .cancelled:
print(gesture.state == .ended ? "Ended" : "Cancelled")
timer?.invalidate()
default:
break
}
}

Related

UIGestureRecognizer.State is 'possible' and not 'recognized' every other click on an MKMapView

I am trying to understand a reproducible bug with my gesture recognisers. I have 2 recognisers on an MKMapView, one UITapGestureRecognizer and one UILongPressGestureRecogniser. Both of them work as expected the first time, however, if I use the long press (which adds an annotation to the map) the next tap gesture will return in the 'possible' state but never hit the 'recognized' state.
▿ Optional<Array<UIGestureRecognizer>>
▿ some : 2 elements
- 0 : <UITapGestureRecognizer: 0x7fda7543ebc0; state = Possible; view = <MKMapView 0x7fda78026e00>>
- 1 : <UILongPressGestureRecognizer: 0x7fda7543e8c0; state = Possible; view = <MKMapView 0x7fda78026e00>; numberOfTapsRequired = 0; minimumPressDuration = 0.2>
After I tap once, and nothing happens, a second tap will then perform the associated function i.e. make it to the recognized state.
I am intercepting all the clicks on the window and the tap definitely takes place each time but the first one after a long press never seems to become accepted. Is there something I'm missing here? The gestures are added as below:
let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
mapView.addGestureRecognizer(mapTap)
let pressGesture = UILongPressGestureRecognizer(target: self, action: #selector(mapLongPress(_:)))
pressGesture.minimumPressDuration = 0.2
pressGesture.numberOfTouchesRequired = 1
mapView.addGestureRecognizer(pressGesture)
Could this be to do with the other gestures which are added by default on an MKMapView?
I tried using your code and got the same result.
I solved it with a tricky solution. I hope it would be helpful for you
mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
mapTap.delegate = self
mapView.addGestureRecognizer(mapTap)
pressGesture = UILongPressGestureRecognizer(target: self, action:
#selector(mapLongPress(_:)))
pressGesture.minimumPressDuration = 0.2
pressGesture.numberOfTouchesRequired = 1
mapView.addGestureRecognizer(pressGesture)
#objc func mapTapped(_ gesture: UITapGestureRecognizer) {
// your code
}
#objc func mapLongPress(_ gesture: UILongPressGestureRecognizer) {
// your code
if gesture.state == .began {
mapTap.isEnabled = false
} else if gesture.state == .cancelled || gesture.state == .ended {
mapTap.isEnabled = true
}
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
In your case you expect that the tap recognizer as well as the long press recognizer operate simultaneously: When you tap the view, both should start the recognition process. When you end the tap before the minimum tap time for the long press, the tap gesture should fire, but when you end the tap later, the long press gesture should fire.
But the Apple docs say:
UIKit normally allows the recognition of only one gesture at a time on
a single view. Recognizing only one gesture at a time is usually
preferable because it prevents user input from triggering more than
one action at a time. However, this default behavior can introduce
unintended side effects. For example, in a view that contains both pan
and swipe gesture recognizers, swipes are never recognized. Because
the pan gesture recognizer is continuous, it always recognizes its
gesture before the swipe gesture recognizer, which is discrete.
In your case, the long tap gesture recognizer is continuous while the tap gesture recognizer is discrete, so there could be a problem in recognizing the tap.
I would thus try to explicitly allow both recognizers to simultaneous recognice their gestures. An example how to do this is given here.
As soon as the long press recognizer fires, you could cancel the recognition operation of the tap recognizer.
Hope this helps!

Detect when a touch begins anywhere on screen even on a UIButton

I am looking for a simple way to detect when the user starts to touch the screen regardless of whether they are touching on a UIButton or elsewhere on the screen.
If I use touchesBegan(... on my ViewController, it does not detect touches on controls like UIButtons.
There is UITapGesturReconizer on ViewController but that would fire only when the tap has completed. I am looking to detect when any touch begins.
Use a UILongPressGestureRecognizer and set its minimumPressDuration to 0. It will act like a touch down during the UIGestureRecognizerStateBegan state.
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()
}
}

(swift) set long press to -= 1 but now number goes down twice

So I have my long press recognizer set as follows:
#IBAction func p1s1scoreLongTouch(_ sender: AnyObject) {
p1s1currentscore -= 1
p1s1scoreoutlet.setTitle(String(p1s1currentscore), for: UIControlState.normal)
}
But now when you long press the button it changes the p1s1current score down two points, once when the duration of the long press is met, and another when lifting finger. How do I remedy this problem?
You are not taking into account the fact that your gesture recognizer action handler can (and almost surely will) be called multiple times in the course of a single gesture. You need to look at the gesture recognizer's state and decide whether you want to perform your decrement for this particular state. (The gesture recognizer is arriving as sender, but you are just ignoring it — foolishly.)
Thanks for the help guys. After learning I was using the wrong syntax I did some research and my updated code looks more like
#IBAction func p1s1scoreLongTouch(_ sender: UILongPressGestureRecognizer) {
if sender.state == .began {
print("UIGestureRecognizerStateBegan")
p1s1currentscore -= 1
p1s1scoreoutlet.setTitle(String(p1s1currentscore), for: UIControlState.normal)
}
else if sender.state == .ended {
print("UIGestureRecognizerStateEnded")
}
}
Still not sure if this is the correct syntax but I know it is more correct since it works.

UIGestureRecognizerState.Ended is not called in some cases

When I drag the view and let it go in a new location .Ended is called everytime.
When I drag the view for a while and let it go in the same location it started, nothing is called, I expect .Ended or default to be called but doesn't happen.
Why does this happen? How can I learn when the user let go of the view consistently?
var gestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("dragged:"))
gestureRecognizer.delegate = self
view.addGestureRecognizer(gestureRecognizer)
func dragged(gesture: UIPanGestureRecognizer) {
switch gesture.state {
case UIGestureRecognizerState.Began:
print("calls this everytime touch began")
case UIGestureRecognizerState.Ended:
print("doesn't call this everytime")
default:
print("never calls this")
}
}
You should almost always use UIGestureRecognizerState.Ended || UIGestureRecognizerState.Cancelled as one of the two will definitely be called at the end of a gesture. This way you can also handle cases where the user has dragged past the screen.
This happened to me when I had a case for .recognized in my switch statement (don't do this).
In my case this happened only when I used two-finger pinch or rotate gestures in the Simulator, using the option key on the keyboard.
Usually pressing the option key once more after the gesture has ended (without moving the mouse) seems to resolve the issue and the .ended state is reached.
I did not encounter this problem on an actual device.

iOS: Detect long press without removing finger

I am using a UILongPressGestureRecognizer. I am wondering if anyone has ever figured out a way to trigger when the minimumPressDuration has been reached without having to lift their fingers. Basically, can we trigger the end of the gesture without having to remove our fingers? Can we just use how much time had passed?
Thanks,
Collin
That's what the UIGestureStateBegan is for:
-(void)handleGesture:(UILongPressGestureRecognizer *)sender
{
if(sender.state == UIGestureRecognizerStateBegan){
NSLog(#"minimum duration elapsed");
}else if(sender.state == UIGestureRecognizerStateEnded){
NSLog(#"user lifted their finger");
}
}
.began state ends when user lifts the finger, so the accepted answer is not correct for this problem. After some time I figured it out.
Answer in Swift:
var gesture = UILongPressGestureRecognizer(target: self, action: #selector(longPress(_:)))
func handleLongPress(){
//do something
gesture.addTarget(self, action: #selector(longPress(_:)))
}
#objc func longPress(_ sender: UILongPressGestureRecognizer) {
if sender.state == .began {
handleLongPress()
gesture.removeTarget(self, action: #selector(longPress(_:)))
}
}
This will cause the gesture to stop tracking touch after .began state is received and re-enable it after you finish doing whatever you want to do after long press
This is very old question but I hope someone will find it useful

Resources