Unproblematic use of UIPinchGestureRecognizer with UILongTapGestureRecognizer - ios

I need to use a UILongTapGestureRecognizer with a UIPinchGestureRecognizer simultaneously.
Unfortunately, the first touch of the UILongTapGestureRecognizer will be counted for the PinchGestureRecognizer too. So when holding UILongTapGestureRecognizer there just needs to be one more touch to trigger the Pinch Recognizer. One is used for long press gesture and two for the pinch.
Is there a way to use both independently? I do not want to use the touch of the UILongTapGestureRecognizer for my UIPinchGestureRecognizer.
This is how I enable my simultaneous workforce:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
//Allowing
if (gestureRecognizer == zoom) && (otherGestureRecognizer == longTap) {
print("working while filming")
return true
}
return false
}

I don't believe you have the tool for what you are looking for so I suggest you try to create your own gesture recognizer. It is not really that hard but you will unfortunately need to do both the long press and the pinch effect.
Don't try overriding UIPinchGestureRecognizer nor UILongPressGestureRecognizer because it will simply not work (or if you mange it please do share your findings). So just go straight for UIGestureRecognizer.
So to begin with long press gesture recognizer we need to track that user presses and holds down for long enough time without moving too much. So we have:
var minimumPressDuration = UILongPressGestureRecognizer().minimumPressDuration
var allowableMovement = UILongPressGestureRecognizer().allowableMovement
Now touches need to be overridden (this is all in subclass of gesture recognizer):
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
touchMoveDistance = 0.0 // Reset the movement to zero
previousLocation = touches.first?.location(in: view) // We need to save the previous location
longPressTimer = Timer.scheduledTimer(timeInterval: minimumPressDuration, target: self, selector: #selector(onTimer), userInfo: nil, repeats: false) // Initiate a none-repeating timer
if inProgress == false { // inProgress will return true when stati is either .began or .changed
super.touchesBegan(touches, with: event)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
if let newLocation = touches.first?.location(in: view), let previousLocation = previousLocation {
self.previousLocation = newLocation
touchMoveDistance += abs(previousLocation.y-newLocation.y) + abs(previousLocation.x-newLocation.x) // Don't worry about precision of this. We can't know the path between the 2 points anyway
}
if inProgress == false {
super.touchesMoved(touches, with: event)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
longPressTimer?.invalidate()
longPressTimer = nil
if inProgress {
state = .ended
}
super.touchesEnded(touches, with: event)
if self.isEnabled { // This will simply reset the gesture
self.isEnabled = false
self.isEnabled = true
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
longPressTimer?.invalidate()
longPressTimer = nil
if inProgress {
state = .ended
}
super.touchesCancelled(touches, with: event)
if self.isEnabled {
self.isEnabled = false
self.isEnabled = true
}
}
So these are all only for long press. And what happens on timer is:
#objc private func onTimer() {
longPressTimer?.invalidate()
longPressTimer = nil
if state == .possible {
state = .began
}
}
So basically if we change state to .begin we trigger the gesture and rest of the events simply work. Which is pretty neat.
Unfortunately this is far from over and you will need to play around a bit with the rest of the code...
You will need to preserve touches (If I remember correctly the same touch will be reported as a comparable object until user lifts his finger):
On begin save the touch to a private property longPressTouch
On timer when long press succeeds set a property to indicate the long press has indeed triggered didSucceedLongPress = true
On begin check if another touch can be added and cancel gesture if it may not if longPressTouch != nil && didSucceedLongPress == false { cancel() }. Or allow it, this really depends on what you want.
On begin if touches may be added then add them in array and save their initial positions pinchTouches.append((touch: touch, initialPosition: touchPosition))
On touches end and cancel make sure to remove appropriate touches from array. And if long press is removed cancel the event (or not, your choice again)
So this should be all the data you need for your pinch gesture recognizer. Since all the events should already be triggering for you the way you need it all you need is a computed value for your scale:
var pinchScale: CGFloat {
guard didSucceedLongPress, pinchTouches.count >= 2 else {
return 1.0 // Not having enough data yet
}
return distanceBetween(pinchTouches[0].touch, pinchTouches[1].touch)/distanceBetween(pinchTouches[0].initialPosition, pinchTouches[1].initialPosition) // Shouldn't be too hard to make
}
Then there are some edge cases you need to check like:
user initiates a long press, uses 2 fingers to pinch, adds 3rd finger (ignored), removes 2nd finger: Without handling this you might get a little jump which may or may not be intended. I guess you could just cancel the gesture or you could somehow transform the initial values to make the jump disappear.
So good luck if you will be implementing this and let us know how it went.

Related

UIPinchGestureRecognizer doesn't fire after custom UIGestureRecognizer fails

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
}
}

How to make a Button continually call a function when held down (SpriteKit)

I'm making a game using sprite kit and I want my Character to move across the screen when you hold down the left/right move button. The problem is that he only moves when the button is tapped, not held. I have looked everywhere for a solution but nothing seems to work!
Here's my code;
class Button: SKNode
{
var defaultButton: SKSpriteNode // defualt state
var activeButton: SKSpriteNode // active state
var timer = Timer()
var action: () -> Void
//default constructor
init(defaultButtonImage: String, activeButtonImage: String, buttonAction: #escaping () -> Void )
{
//get the images for both button states
defaultButton = SKSpriteNode(imageNamed: defaultButtonImage)
activeButton = SKSpriteNode(imageNamed: activeButtonImage)
//hide it while not in use
activeButton.isHidden = true
action = buttonAction
super.init()
isUserInteractionEnabled = true
addChild(defaultButton)
addChild(activeButton)
}
//When user touches button
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
{
action()
//using timer to repeatedly call action, doesnt seem to work...
self.timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(getter: Button.action), userInfo: nil, repeats: true)
//swtich the image of our button
activeButton.isHidden = false
defaultButton.isHidden = true
}
code..........
In my game scene...
// *** RIGHT MOVEMENT ***
let rightMovementbutton = Button(defaultButtonImage: "arrow", activeButtonImage: "arrowActive", buttonAction:
{
let moveAction = SKAction.moveBy(x: 15, y: 0, duration: 0.1)
self.player.run(moveAction)
})
You know when the button is touched because touchesBegan is called. You then have to set a flag to indicate that the button is pressed.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first!
if leftButton.containsPoint(touch.locationInNode(self)) {
leftButtonIsPressed = true
}
if rightButton.containsPoint(touch.locationInNode(self)) {
rightButtonIsPressed = true
}
}
In update(), call your function that flag is true:
update() {
if leftButtonIsPressed == true {
moveLeft()
}
if rightButtonIsPressed == true {
moveRight()
}
}
You set the flag to false when touchesEnded is called for that button:
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first!
if leftButton.containsPoint(touch.locationInNode(self)) {
leftButtonIsPressed = false
}
if rightButton.containsPoint(touch.locationInNode(self)) {
rightButtonIsPressed = flase
}
}
Edit:
As pointed out by KoD, a cleaner way to do this (for movement buttons) is with SKAction which removes the need for the flag:
Define SKActions for moveTo x:0 and moveTo x:frame.width in
didMoveTo(View:)
In touchesBegan, run the correct SKAction on the correct object
specifying a key for the SKAction.
In touchesEnded, remove the relevant SKAction.
You'll have to do some maths to calculate how many points your object will have to move and then set a duration for the SKAction based upon this distance and a movement speed (in points per second).
Alternatively, (thanks to KnightOfDragons for this) create a SKAction.MoveBy x: which moves a small distance (based upon your desired movement speed) and with a duration of 1/60s. Repeat this action forever (SKAction.repeatForever) when the button is touched and remove the repeating SKAction when the button is released.

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.

Disable touches everywhere except inside of a button? - Swift

When I die in my game, I want to ignore all touch events by the user EXCEPT if the user taps inside of or on the reset game button. Here is my code.
for touch in touches{
let location = touch.locationInNode(self)
if died == true{
Ball.physicsBody?.velocity = CGVectorMake(0, 0)
if resetGame.containsPoint(location){
restartScene()
runAction(SKAction.playSoundFileNamed("Woosh.wav", waitForCompletion: false))
}
else {
self.userInteractionEnabled = false
}
This is all inside of my touchesBegan. This was my attempt at ignoring the user's touch unless the location of the touch was within the size of button. How can I ignore a user's touches everywhere on the screen except the button? resetGame is an SKSpriteNode image.
There are two solutions to your issue.
The first case I want to propose to you is based to gesture recognizer.
You can separate the button from the other touches events and switch on/off the touches event by a boolean var like this:
Gesture recognizers:
In the global var declaration section of your class:
var tapGesture :UITapGestureRecognizer!
var enableTouches:Bool = true
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
self.tapGesture = UITapGestureRecognizer(target: self, action: #selector(GameClass.simpleTap(_:)))
self.tapGesture.cancelsTouchesInView = false
view.addGestureRecognizer(tapGesture)
myBtn.name = "myBtn"
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
return self.enableTouches
}
func simpleTap(sender: UITapGestureRecognizer) {
print("simple tap")
if sender.state == .Ended {
var touchLocation: CGPoint = sender.locationInView(sender.view)
touchLocation = self.convertPointFromView(touchLocation)
let touchedNode = self.nodeAtPoint(touchLocation)
// do your stuff
if touchedNode.name == "myBtn" {
// button pressed, do your stuff
}
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if died == true {
self.enableTouches = false // disable all touches but leave gestures enabled
//do whatever you want when hero is died
}
}
Only a Boolean
The second solution I want to propose is simply to stopping touches flow by using a simple boolean (it's not very elegant but it works).
This method look when button is tapped and the update method check if your hero is dead so all touches will be disabled:
In the global var declaration section of your class:
var enableTouches:Bool = true
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
myBtn.name = "myBtn"
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (!enableTouches) {
return
}
let touch = touches.first
let positionInScene = touch!.locationInNode(self)
let touchedNode = self.nodeAtPoint(positionInScene)
// do your stuff
if touchedNode.name == "myBtn" {
// button pressed, do your stuff
if died == true {
self.enableTouches = false // disable all touches
//do whatever you want when hero is died
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (!enableTouches) {
return
}
let touch = touches.first
let positionInScene = touch!.locationInNode(self)
let touchedNode = self.nodeAtPoint(positionInScene)
// do your stuff
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if (!enableTouches) {
return
}
}
The first case permit to you to have always the gesture enabled so you can do also other stuff with gestures when your hero will be died. The second case stop your touches when you press your button and do the "die flow". Choose which may be the most suitable for you.

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