Swift Game - Tap and Tap + Hold Gestures? - swift

I'm in the process of learning swift (and spritekit) and trying to make a simple game.
In my game, the hero should jump or duck...
The hero needs to jump when the screen is tapped,or duck if the screen is tap+held (long gesture)
So basic pseudo code:
if tapped
heroJump()
else if tappedAndHeld
heroDuck()
I have a func which is seen in almost all tutorials, which handles the touch event:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self) //need location of tap for something
switch gameState {
case .Play:
//do in game stuff
break
case .GameOver:
//opps game ended
}
break
}
}
super.touchesBegan(touches, withEvent: event)
}
Is there a way, to include in this touch event, to decide if it was tapped or held? I can't seem to get my head around the fact, the program will always recognise a tap before a long gesture?!?
Anyway, in an attempt to solve my problem, I found THIS question, which introduced to me recognisers, which I tried to implement:
override func didMoveToView(view: SKView) {
// other stuff
//add long press gesture, set to start after 0.2 seconds
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: "longPressed:")
longPressRecognizer.minimumPressDuration = 0.2
self.view!.addGestureRecognizer(longPressRecognizer)
//add tap gesture
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tapped:")
self.view!.addGestureRecognizer(tapGestureRecognizer)
}
And these are the functions the gestures call:
func longPressed(sender: UILongPressGestureRecognizer)
{
if (sender.state == UIGestureRecognizerState.Ended) {
print("no longer pressing...")
} else if (sender.state == UIGestureRecognizerState.Began) {
print("started pressing")
// heroDuck()
}
}
func tapped(sender: UITapGestureRecognizer)
{
print("tapped")
// heroJump()
}
How can I combine these two things?
Can I add a way to determine whether it was tapped or hold in my touchBegins event, or can I scrap that method and use only the two functions above?
One of many problems being getting the location if using the latter?
Or maybe I'm looking at this completely wrong, and there's a simple and/or built in way in swift/spritekit?
Thanks.

You only need the UITapGestureRecognizer and the UILongPressRecognizer. You do not need to do anything with touchesBegan and touchesEnded, because the gesture recognizer analyses the touches itself.
To get the location of the touch you can call locationInView on the gesture recognizer to get the location of the gesture or locationOfTouch if you are working with multitouch gestures and need the location of each touch. Pass nil as parameter when you want the coordinates in the window’s base coordinate system.
Here is a working example:
func setupRecognizers() {
let tapRecognizer = UITapGestureRecognizer(target: self, action: Selector("handleTap:"))
let longTapRecognizer = UILongPressGestureRecognizer(target: self, action: Selector("handleLongPress:"))
view.addGestureRecognizer(tapRecognizer)
view.addGestureRecognizer(longTapRecognizer)
}
func handleLongPress(recognizer: UIGestureRecognizer) {
if recognizer.state == .Began {
print("Duck! (location: \(recognizer.locationInView(nil))")
}
}
func handleTap(recognizer: UIGestureRecognizer) {
print("Jump! (location: \(recognizer.locationInView(nil))")
}
If a long press is recognized handleTap: tap is not called. Only if the user lifts his finger fast enough handleTap: will be called. Otherwise handleLongPress will be called. handleLongPress will only be called after the long press duration has passed. Then handleLongPress will be called twice: When the duration has passed ("Began") and after the user has lifted his finger ("Ended").

you do the same thing you are doing for longpress, wait till the .Ended event
func tapped(sender: UITapGestureRecognizer)
{
if sender.state == .Ended {
print("tapped")
}
}
A tap event will always happen, this can't be prevented because lets face it, you need to touch the screen. What should be happening though is when you enter the long press event, the tap event should go into a Cancel state instead of an Ended state

Related

How would I make a UiPanGestureRecognizer check if the finger is in the buttons frame?

I am trying to make an app where the user could drag a finger on top of multiple buttons and get some actions for each button.
There was a similar question from a while back but when I tried to use CGRectContainsPoint(button.frame, point) it said it was replaced with button.frame.contains(point) but this didn’t seem to work for me. Here is a link to the Photo
What I have done so far:
var buttonArray:NSMutableArray!
override func viewDidLoad()
{
super.viewDidLoad()
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureMethod(_:)))
a1.addGestureRecognizer(panGesture)
a2.addGestureRecognizer(panGesture)
}
#objc func panGestureMethod(_ gesture: UIPanGestureRecognizer) {
if gesture.state == UIGestureRecognizer.State.began {
buttonArray = NSMutableArray()
}
let pointInView = gesture.location(in: gesture.view)
if !buttonArray.contains(a1!) && a1.frame.contains(pointInView) {
buttonArray.add(a1!)
a1Tapped(a1)
}
else if !buttonArray.contains(a2!) && a2.frame.contains(pointInView) {
buttonArray.add(a2!)
a2Tapped(a2)
}
The code did run fine but when I tried to activate the drag nothing happened. Any tips?
You want behavior similar to the system keyboard I assume? CGRectContainsPoint is not deprecated: See the docs. In Swift it's written like frame.contains().
When dealing with rects and points you have to make sure both are translated to the same coordinate system first. To do so you can use the convert to/from methods on UIView: See (the docs).
In your case smth. like the following should work (first translate button frames, then check if the point is inside):
func touchedButtonForGestureRecognizer(_ gesture: UIPanGestureRecognizer) -> UIView? {
let pointInView = gesture.location(in: gesture.view)
for (button in buttonArray) {
let rect = gesture.view.convert(button.frame from:button.superview)
if (rect.contains(pointInView)) {
return button
}
}
return nil
}

swift SpriteNode and SceneKit multiple touch gestures

I am making a word game. For this I am using SceneKit and adding a SpriteNodes to represent letter tiles.
The idea is that when a user clicks on a letter tile, some extra tiles appear around it with different letter options. My issue is regarding the touch gestures for various interactions.
When a user taps on a letter tile, additional tiles are shown. I have achieved this using the following method in my tile SpriteNode class:
override func touchesBegan(_ touches:Set<UITouch> , with event: UIEvent?) {
guard let touch = touches.first else {
return
}
delegate?.updateLetter(row: row, column: column, x:xcoord, y:ycoord, useCase: 1)
}
This triggers the delegate correctly which shows another sprite node.
What I would like to achieve is for a long press to remove the sprite node from parent. I have found the .removeFromParent() method, however I cannot get this to detect a long press gesture.
My understanding is that this type of gesture must be added using UIGestureRecognizer. I can add the following method to my Scene class:
override func didMove(to view: SKView) {
let longPress = UILongPressGestureRecognizer(target: self,
action: #selector(GameScene.longPress(sender:)))
view.addGestureRecognizer(longPress)
}
#objc func longPress(sender: UILongPressGestureRecognizer) {
print("Long Press")
This will detect a long press anywhere on the scene. However I need to be able to handle the pressed nodes properties before removing it. I have tried adding the below to the longPress function:
let location = sender.location(in: self)
let touchedNodes = nodes(at: location)
let firstTouchedNode = atPoint(location).name
touchedNodes[0].removeFromParent()
but I get the following error: Cannot convert value of type 'GameScene' to expected argument type 'UIView?'
This seems a little bit of a messy way of doing things, as I have touch methods in different places.
So my question is, how can I keep the current touchesBegan method that is in the tile class, and add a long press gesture to be able to reference and delete the spriteNode?
Long press gestures are continuous gestures that may be called multiple times as you are seeing. Have you tried Recognizer.State.began, .changed, .ended? I solved a similar problem doing things this way.
EDIT - I think one way to get there is to get your object on handleTap and hang on to the object. Then when LongPress happens, you already have your node. If something changes before longPress, obviously you need to reset. Sorry, this is some extra code on here, but look at hitTest.
#objc func handleTap(recognizer: UITapGestureRecognizer)
{
let location: CGPoint = recognizer.location(in: gameScene)
if(data.isAirStrikeModeOn == true)
{
let projectedPoint = gameScene.projectPoint(SCNVector3(0, 0, 0))
let scenePoint = gameScene.unprojectPoint(SCNVector3(location.x, location.y, CGFloat(projectedPoint.z)))
gameControl.airStrike(position: scenePoint)
}
else
{
let hitResults = gameScene.hitTest(location, options: hitTestOptions)
for vHit in hitResults
{
if(vHit.node.name?.prefix(5) == "Panel")
{
// May have selected an invalid panel or auto upgrade was on
if(gameControl.selectPanel(vPanel: vHit.node.name!) == false) { return }
return
}
}
}
}
So I am not completely satisfied with this answer, however it is a work around for what I need.
What I have done is added two variables ‘touchesStart’ and ‘touchesEnd’ to my tiles class.
Then in touchesBegan() I add a call to update touchesStart with CACurrentMediaTime() and update touchesEnd via the touchesEnded() function.
Then in the touchesEnded() I subtract touchesStart from touchesEnd. If the difference is more than 1.0 I call the function for long press. If less than 1.0 I call the function for tap.

Conflict between Pan Gesture and Tap Gestures

I'm currently working on a game that uses UIGestureRecognizers. I'm using the pan gesture to move the player around and the tap gesture to detect other UI button touches. Everything seems to work fine except that there is a conflict between the 2 gestures. Whenever the player is on the move (pan gesture gets recognized) the game ignores all my tap gestures (Once the pan gesture is recognized, the view won't recognize tap gestures).
Can someone please show me how to make the 2 gestures work together. I want the player to stop moving when a UI button gets tapped. In another word, I want to cancel the pan gesture whenever a tap gesture is recognized.
Thank you so much in advance!
Here is how I setup the 2 gestures:
let singleTap = UITapGestureRecognizer(target: self, action: #selector(doSingleTap))
singleTap.numberOfTapsRequired = 1
singleTap.delegate = self
self.view?.addGestureRecognizer(singleTap)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
panGesture.minimumNumberOfTouches = 1
panGesture.delegate = self
self.view?.addGestureRecognizer(panGesture)
#objc func handlePan(gestureReconizer: UIPanGestureRecognizer) {
if isPaused || player.isInAction {return}
let translation = gestureReconizer.translation(in: self.view)
if gestureReconizer.state == .changed {
let angle = 180 + (atan2(-translation.x, translation.y) * (180/3.14159))
player.movementAngle = angle
player.atState = .Walk
}
if gestureReconizer.state == .ended {
player.movementAngle = 0.0
if player.atState == .Walk {
player.atState = .Idle
}
}
}
#objc func doSingleTap(gestureReconizer: UITapGestureRecognizer) {
let originaTapLocation = gestureReconizer.location(in: self.view)
let location = convertPoint(fromView: originaTapLocation)
let node = atPoint(location)
switch node.name {
case "HeroAvatar":
//do stuff here
break
case "Fire":
//do stuff here
break
case "Potion":
//do stuff here
break
default:
break
}
}
You need to implement delegate method of UIGestureRecognizerDelegate like below:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// Don't recognize a single tap gesture until a pan gesture fails.
if gestureRecognizer == singleTap &&
otherGestureRecognizer == panGesture {
return true
}
return false
}
Hope this will work for you :)
For more info Apple Doc Preferring One Gesture Over Another

iOS swift double tap handle button and map

I had mapbox map and few buttons on it. Problem is when user double tap any button maps zoom in. How to disable this? Even when user double tap zoom out, map is zoomed in and after that zoomed out. I try but in iOS I don't know how, for android I know the solution. Below code doesn't work when double tap button map is zoomed in and also button delegate double tap ...
let singleTap4 = UITapGestureRecognizer(target: self, action: #selector(MainViewController.tapPatrolaBtn))
singleTap4.numberOfTapsRequired = 1
singleTap4.numberOfTouchesRequired = 1
patrolaButton.isUserInteractionEnabled = true
patrolaButton.addGestureRecognizer(singleTap4)
patrolaButton.addTarget(self, action: #selector(multipleTap(_:event:)), for: UIControlEvents.touchDownRepeat)
#objc func multipleTap(_ sender: UIButton, event: UIEvent) {
let touch: UITouch = event.allTouches!.first!
if (touch.tapCount == 2) {
}
}
Here you need to handle double tap gesture too.
If you are using double tap gesture recogniser then double tapevent will not be delegate to mapbox map and mapbox zoom event wont call
Use below code for same:-
button.addTarget(self, action: #selector(multipleTap(_:event:)), for: UIControlEvents.touchDownRepeat)
//Don't do in action
func multipleTap(_ sender: UIButton, event: UIEvent) {
let touch: UITouch = event.allTouches!.first!
if (touch.tapCount == 2) {
}
}
I find solution, on button click I run this code so it disable map gesture for a while :
mapView.isZoomEnabled = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
self.mapView.isZoomEnabled = true }

How do I capture the point initially tapped in a UIPanGestureRecognizer?

I have an app that lets the user trace lines on the screen. I am doing so by recording the points within a UIPanGestureRecognizer:
-(void)handlePanFrom:(UIPanGestureRecognizer *)recognizer
{
CGPoint pixelPos = [recognizer locationInView:rootViewController.glView];
NSLog(#"recorded point %f,%f",pixelPos.x,pixelPos.y);
}
That works fine. However, I'm very interested in the first point the user tapped before they began panning. But the code above only gives me the points that occurred after the gesture was recognized as a pan (vs. a tap.)
From the documentation, it appears there may be no easy way to determine the initially-tapped location within the UIPanGestureRecognizer API. Although within UIPanGestureRecognizer.h, I found this declaration:
CGPoint _firstScreenLocation;
...which appears to be private, so no luck. I'm considering going outside the UIGestureRecognizer system completely just to capture that initailly-tapped point, and later refer back to it once I know that the user has indeed begun a UIPanGesture. I Thought I would ask here, though, before going down that road.
Late to the party, but I notice that nothing above actually answers the question, and there is in fact a way to do this. You must subclass UIPanGestureRecognizer and include:
#import <UIKit/UIGestureRecognizerSubclass.h>
either in the Objective-C file in which you write the class or in your Swift bridging header. This will allow you to override the touchesBegan:withEvent method as follows:
class SomeCoolPanGestureRecognizer: UIPanGestureRecognizer {
private var initialTouchLocation: CGPoint!
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent) {
super.touchesBegan(touches, withEvent: event)
initialTouchLocation = touches.first!.locationInView(view)
}
}
Then your property initialTouchLocation will contain the information you seek. Of course in my example I make the assumption that the first touch in the set of touches is the one of interest, which makes sense if you have a maximumNumberOfTouches of 1. You may want to use more sophistication in finding the touch of interest.
Edit: Swift 5
import UIKit.UIGestureRecognizerSubclass
class InitialPanGestureRecognizer: UIPanGestureRecognizer {
private var initialTouchLocation: CGPoint!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
initialTouchLocation = touches.first!.location(in: view)
}
}
You should be able to use translationInView: to calculate the starting location unless you reset it in between. Get the translation and the current location of touch and use it to find the starting point of the touch.
#John Lawrence has it right.
Updated for Swift 3:
import UIKit.UIGestureRecognizerSubclass
class PanRecognizerWithInitialTouch : UIPanGestureRecognizer {
var initialTouchLocation: CGPoint!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
initialTouchLocation = touches.first!.location(in: view)
}
}
Note that the instance variable initialTouchLocation cannot be private, if you want to access it from your subclass instance (handler).
Now in the handler,
func handlePan (_ sender: PanRecognizerWithInitialTouch) {
let pos = sender.location(in: view)
switch (sender.state) {
case UIGestureRecognizerState.began:
print("Pan Start at \(sender.initialTouchLocation)")
case UIGestureRecognizerState.changed:
print(" Move to \(pos)")
You could use this method:
CGPoint point = [gesture locationInView:self.view];
I have also noticed that when I attempt to read the value in the shouldBegin method on a UIPanGestureRecognizer, I only see its location after the user moved a little bit (i.e. when the gesture is beginning to recognize a pan). It would be very useful to know where this pan gesture actually started though so that I can decide if it should recognize or not.
If you don't want to subclass UIGestureRecognizer view, you have two options:
UILongPressGestureRecognizer, and set delay to 0
UIPanGestureRecognizer, and capture start point in shouldReceiveTouch
If you have other gestures (e.g. tap, double tap, etc), then you'll likely want option 2 because the long press gesture recognizer with delay of 0 will cause other gestures to not be recognized properly.
If you don't care about other gestures, and only want the pan to work properly, then you could use a UILongPressGestureRecognizer with a 0 delay and it'll be easier to maintain cause you don't need to manually keep track of a start point.
Solution 1: UILongPressGestureRecognizer
Good for: simplicity
Bad for: playing nice with other gesture handlers
When creating the gesture, make sure to set minimumPressDuration to 0. This will ensure that all your delegate methods (e.g. should begin) will receive the first touch properly.
Because a UILongPressGestureRecognizer is a continuous gesture recognizer (as opposed to a discrete gesture recognizer), you can handle movement by handling the UIGestureRecognizer.State.changed property just as you would with a UIPanGestureRecognizer (which is also a continuous gesture recognizer). Essentially you're combining the two gestures.
let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(gestureHandler(_:))
gestureRecognizer.minimumPressDuration = 0
Solution 2: UIPanGestureRecognizer
Good for: Playing nicely with other gesture recognizers
Bad for: Takes a little more effort saving the start state
The steps:
First, you'll need to register as the delegate and listen for the shouldReceiveTouch event.
Once that happens, save the touch point in some variable (not the gesture point!).
When it comes time to decide if you actually want to start the gesture, read this variable.
var gestureStartPoint: CGPoint? = nil
// ...
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(gestureHandler(_:))
gestureRecognizer.delegate = self
// ...
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
gestureStartPoint = touch.location(in: self)
return true
}
Warning: Make sure to read touch.location(in: self) rather than gestureRecognizer.location(in: self) as the former is the only way to get the start position accurately.
You can now use gestureStartPoint anywhere you want, such as should begin:
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return isValidStartPoint(gestureStartPoint!)
}
in the same UIView put in this method.
//-----------------------------------------------------------------------
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint point = [[[event allTouches] anyObject] locationInView:self];
NSLog(#"point.x ,point.y : %f, %f",point.x ,point.y);
}
look for it in the UIGestureRecognizer Class Reference here:
https://developer.apple.com/library/ios/documentation/uikit/reference/UIGestureRecognizer_Class/Reference/Reference.html
You can use UIGestureRecognizerStateBegan method. Here is the link to Apple documentation on UIGestureRecognizer class.
http://developer.apple.com/library/ios/ipad/#documentation/uikit/reference/UIGestureRecognizer_Class/Reference/Reference.html%23//apple_ref/occ/cl/UIGestureRecognizer
Wow, A couple years late.. I ran into this problem, but solved it with a static variable:
- (IBAction)handleGesture:(UIPanGestureRecognizer *)recog {
CGPoint loc = [recognizer locationInView:self.container];
static CGFloat origin = 0.0f;
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:
origin = loc.x;
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStatePossible:
// origin is still set here to the original touch point!
break;
case UIGestureRecognizerStateEnded:
break;
case UIGestureRecognizerStateFailed:
case UIGestureRecognizerStateCancelled:
break;
}
}
The variable is only set when recognizer state is UIGestureRecognizerBegan. Since the variable is static, the value persists to later method calls.
For my case, I just needed the x coordinate, but you can change it to a CGPoint if you need.