UIPinchGestureRecognizer doesn't fire after custom UIGestureRecognizer fails - ios

I have a custom UIGestureRecognizer subclass that detects a particular kind of one-finger dragging. On the same scene, I also have a UIPinchGestureRecognizer, that detects two-finger pinches.
The problem happens when the user makes a pinch gesture but puts down one finger an instant before the other: the custom gesture recognizer sees a single touch and engages (sets its state to .began) and the pinch recognizer doesn't. When the second touch is added, the custom recognizer notices and switches its state to .failed. But it's too late and the pinch gesture recognizer doesn't pick it up.
Here's the code for the custom UIGestureRecognizer subclass:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
guard touches.count == 1 else {
state = .failed
return
}
state = .began
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard touches.count == 1 else {
state = .failed
return
}
// do gesture recognizer stuff here
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
state = .ended
}
I've tried it with cancelsTouchesInView set to false and with delaysTouchesBegan set to true for the custom gesture recognizer. Neither made a difference in this behavior.
When I use a UIPinchGestureRecognizer with a UIPanGestureRecognizer I don't have this issue, which leads me to think it's not the intended behavior.
What's wrong with my UIGestureRecognizer fail code and how can I fix it so that a UIPinchGestureRecognizer can still recognize pinches where the touches start non-simultaneously?

Try using the UIGestureRecognizerDelegate protocol to implement gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:). That can allow you to have two gestures be recognized simultaneously.
class MyVC: UITableViewController, UIGestureRecognizerDelegate {
override func viewDidLoad() {
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinchFrom))
pinchGestureRecognizer.delegate = self
view.addGestureRecognizer(pinchGestureRecognizer)
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

Related

touchesBegan and touchesMoved also called during pinch

I have added a pinch gesture and a pan gesture in a view. I am also using touchesBegan and touchesMoved method. Surprisingly when i am pinching my view it is sometimes also calling touchesBegan and touchesMoved.How to stop calling this methods during pinching? Here is what i did.
class CustomView: UIView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touchCount = event?.touches(for: self)?.count {
if touchCount == 1 {
// Doing some task
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
if let touchCount = event?.touches(for: self)?.count {
if touchCount == 1 {
// Doing some task
}
}
}
}
In my ViewController: I created a customView object of CustomView class and added some gestures like below:
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(pinch(recognizer:)))
customView.addGestureRecognizer(pinch)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(recognizer:)))
panGesture.minimumNumberOfTouches = 2
self.customView.addGestureRecognizer(panGesture)
touchesBegan and touchesMoved are really, really generic -- whenever your finger touches down or moves, they'll be called. This includes button presses and just pretty much every thing that your finger can touch.
So if you're using gesture recognizers for pan and pinch, stick with gesture recognizers. Use an UITapGestureRecognizer instead of relying on touchesBegan, and another UIPanGestureRecognizer for touchesMoved.

Gesture recognizer with delaysTouchesBegan stops all calls to touchesMoved?

The view controller code below demonstrates my question. It prints some debugging statements in response to touch events and a pinch gesture.
If the gesture recognizer is disabled, and I drag a finger on the screen, everything works as I expected: the controller gets a touchesBegan, a bunch of touchesMoved, and a touchesEnded. But if I enable the gesture recognizer and set its delaysTouchesBegan property to true, and drag a single finger, the controller receives only touchesBegan and touchesEnded calls - no touchesMoved.
The docs for delaysTouchesBegan suggest that touch events will pile up and be delivered after the gesture recognizer fails. Is this the expected behavior - to lose all the touchesMoved events?
class ViewController: UIViewController {
var gr: UIPinchGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
gr = UIPinchGestureRecognizer(target: self, action: #selector(pinched(_:)))
gr.delaysTouchesBegan = true
view.addGestureRecognizer(gr)
}
#IBAction func buttonTapped(_ sender: Any) {
gr.isEnabled = !gr.isEnabled
print("pinch recognizer enabled: \(gr.isEnabled)")
}
#objc func pinched(_ gr: Any?) {
print("pinched")
}
var i = 0
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
i = 0
print("touchesBegan")
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesMoved \(i)")
i += 1
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesEnded")
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesCancelled")
}
}
A pinch gesture recognizer reports changes each time the distance between the fingers change. The distance between the fingers is reported as a scale factor. So you will want to look at the scale factor, not touchesMoved. Was that helpful?
#objc func pinched(_ gr: UIPinchGestureRecognizer) {
if gr.state == .began {
print("began")
}
if gr.state == .changed {
print("scale: \(gr.scale)")
}
if gr.state == .ended {
print("ended")
}
}
Handling Pinch Gestures
Handling Pinch Gestures ( Swift 4 - 2018 )

Tap gesture to NOT occur when touching a specific location - SpriteKit

I have used touchesBegan to provide functionality for my UIButtons and have used a tapped gesture to provide functionality for my main player SKSpriteNode making it jump when triggered.
//Code regarding the UIButton touch
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//touches began is only used for GUI buttons -> not to affect player
for touch: AnyObject in touches {
//We get location of the touch
let locationOfTouch = touch.location(in: self)
if muteButton.contains(locationOfTouch) { //mute the game
timer.invalidate()
audioPlayer.volume = 0
}
//Code regarding the tap
let tap = UITapGestureRecognizer(target: self, action: #selector(GameScene.tapped(gesture:)))
tap.cancelsTouchesInView = false
self.view!.addGestureRecognizer(tap)
......
func tapped(gesture: UIGestureRecognizer) { //used to make the player jump
player.physicsBody!.applyImpulse(CGVector(dx: 0, dy: 60))
player.physicsBody!.affectedByGravity = true */
}
My problem is that when I press on the restartButton the tap gesture is also activated later when the touch ends. Is there anything I can do?
The main issue is that the two separate systems for detecting touches (using gesture recognizers and using the touchesBegan/Moved/Ended methods) are in conflict.
One solution is to enable and disable the gesture recognizer if the touch is inside one of the buttons.
In the touchesBegan method, if the touch is inside a button, disable the tap gesture recognizer:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let locationOfTouch = touch.location(in: self)
if muteButton.contains(locationOfTouch) {
// mute action
tap.isEnabled = false
}
}
}
Then in touchesEnded and touchesCancelled, re-enable the gesture recognizer:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
tap.isEnabled = true
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
tap.isEnabled = true
}
This way, if the touch is inside a button, the tap gesture recognizer will not fire. Whenever any touch is complete, we always re-enable the gesture recognizer in case the next touch is meant to make the player jump.
I have tested this out in an empty project, and it works.
Hopefully that helps! Good luck with your game.

3D Touch force gesture activates cell tap on touch end

I have subclassed a UITableView in my app so that I can intercept touch events. I am using this to allow me to provide 3D Touch gestures on the entire view (including on top of the table view).
This works great, however the problem is that using 3D Touch on one of the cells and then releasing your finger activates the cell tap.
I need to only activate the cell tap if there is no force exerted. I should explain that I am fading an image in gradually over the entire screen as you apply pressure.
Here is my subclass:
protocol PassTouchesTableViewDelegate {
func touchMoved(touches: Set<UITouch>)
func touchEnded(touches: Set<UITouch>)
}
class PassTouchesTableView: UITableView {
var delegatePass: PassTouchesTableViewDelegate?
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesMoved(touches, withEvent: event)
self.delegatePass?.touchMoved(touches)
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesEnded(touches, withEvent: event)
self.delegatePass?.touchEnded(touches)
}
}
And here are the methods I'm calling from my view controller when the touches end and move:
internal func touchMoved(touches: Set<UITouch>) {
let touch = touches.first
self.pressureImageView.alpha = (touch!.force / 2) - 1.0
}
internal func touchEnded(touches: Set<UITouch>) {
UIView.animateWithDuration(0.2, animations: {
self.pressureImageView.alpha = 0.0
})
}
You could create a boolean named isForceTouch which is set to false in touchesBegan, and then once force touch is detected, set it to true. Then in didSelectRowAtIndexPath just return false if isForceTouch is true. It may need tweaking but that should work.

Detect long touch in Sprite Kit

I have a node in a SKScene that I am moving as per the users touch. Basically, this character should also be trying to follow the users finger (Assuming the finger is on the screen). I currently have it implemented as so, which works fine:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
player.runAction(SKAction.moveTo(touch.locationInNode(self), duration: 1))
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
let touch = touches.first as! UITouch
player.runAction(SKAction.moveTo(touch.locationInNode(self), duration: 1))
}
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
player.removeAllActions()
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
player.removeAllActions()
}
However, the problem is that if a user holds his/her finger on the phone. The touchesBegan is only called once, and that's when the tap starts, not when it is held. I want the player character to constantly be trying to reach the finger.
I am centering the camera on the node, so the only time the node should be touching the finger is if the user puts his finger on/in the node (I.e the same position as the node). Because of this, after I run the SKAction to move the node the touch is invalid since it is at the old position.
How would I do this?
You can register a long touch event like this:
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: "longPressed:")
self.view.addGestureRecognizer(longPressRecognizer)
func longPressed(sender: UILongPressGestureRecognizer) {
// your code
}
For Swift 4:
You'll first want to make GameScene a UIGestureRecognizerDelegate:
class GameScene: SKScene, UIGestureRecognizerDelegate {
And so you'll also need to add the delegate method to the GameScene class:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Then inside GameScene's override func didMove(to view: SKView) { add the following:
let pressed:UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPress(sender:)))
pressed.delegate = self
pressed.minimumPressDuration = 0.1
view.addGestureRecognizer(pressed)
Finally inside GameScene add your function to handle the long press (within which you can also discern the long press's state too):
func longPress(sender: UILongPressGestureRecognizer) {
if sender.state == .began { print("LongPress BEGAN detected") }
if sender.state == .ended { print("LongPress ENDED detected") }
}
Add two instance variables to your SKScene, BOOL fingerIsTouching and CGPoint touchLocation.
Inside -touchesBegan:, set fingerIsTouching to YES and update touchLocation to the correct location.
In your SKScene's -update: method, check if fingerIsTouching is YES and move your character according to touchLocation. I recommend using -update: because it is called once per frame, which is exactly enough. You might have to use some other method for moving the character than SKAction though.
Don't forget to update touchLocation in -touchesMoved:, and reset fingerIsTouching inside -touchesCancelled: and -touchesEnded: :)
Sorry for the Obj-C, hope this illustrates the point.

Resources