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.
Related
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.
I have a few button with amorphous look. (the Rect of the buttons are intersecting) First I evaluated the BeganTouch in the GameScene. Then I get several touches.
Since I have in my buttons still child nodes, they have swallowed the touchs. Ok, I have made with the help of here a subclass of the SpriteNodes and processed the touch inside the subclass. Now I have the problem that the first button does not pass the touch to the underlying sprites.
But now I would like top ignore the transparent areas of the buttons, how can I do that?
I have read that one can work with physicsbody, I have tried, gravitiy = 0 but since I move and scale the buttons via actions there were violent effects.
Why can I check for the alpha in the touch location and pass the touch to the next sprite.
by the way: how can I get a reference to the view? to get the global loc.
let loc = touch.location(in: view)
with the global touch location I could check all sprites under this point for the alpha!
you can try passing the touch to it's parent (presumably the scene) from your subclass.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch! {
//handle whatever code you want your subclass to do
...
//pass the touch event to the parent
super.touchesBegan(touches, with: event)
}
}
and then in your scene, cycle through the your buttons (in this example I have the buttons in a button array)
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as UITouch! {
let touchLocation = touch.location(in: self)
for button in buttons {
if button.contains(touchLocation) {
//handle all other buttons
...
}
}
}
}
although this seems a little redundant to do the touches in two different locations. #Knight0fDragon is correct, it seems odd to have a button have transparent areas.
ok, it's simple, all buttons are from the same subclass. this subclass delegates to an method in the GameScene. here I can check with
allNodes = nodes(at: globalLocation)
now I can check for the name of the node, calculate the point of the pixel inside each node and get the alpha value.
thanxs all
My goal is to find out if a UITouch took place within a SpriteKit label node. The view is an SKView. The issue with the code that I am using (below) is that the touch and the rectangle seem to be in different coordinate systems.
I could use simple math to correct this, but is there a simple way to correct this issue? Or is there another way I should be doing this?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let position = t.location(in: view)
//if inside startButton
if (startButton?.frame.contains(position))! {
debugPrint("yes")
}
}
}
I assume that you are are overriding touchesBegan within a scene. If so, then try using let position = t.location(in: self), i.e. replace view with self. That way you get the position within the scene itself, rather than the position within the view that holds the scene.
Hope that helps!
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
I have an SKShapeNode that I have created and given a CGPath to. This is in my GameScene.swift in didMoveToView:
let myNode = SKShapeNode()
let lineToDraw = CGPathCreateMutable()
CGPathMoveToPoint(lineToDraw, nil, 0, 0)
CGPathAddLineToPoint(lineToDraw, nil, 87, 120)
myNode.path = lineToDraw
myNode.strokeColor = SKColor.redColor()
myNode.lineWidth = 20
myNode.name = "My Node!"
world.addChild(myNode) // World is a higher-level node in my scene
And this is how I'm detecting touches, again in the Scene:
override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
if let touch = touches.anyObject() as? UITouch {
if let shapeNode = nodeAtPoint(touch.locationInNode(self)) as? SKShapeNode {
println("Touched \(shapeNode.name)")
} else {
println("Nothing here")
}
}
}
The line node is showing up with no issues. However, it is not registering touchesEnded at all.
Really, I have three questions nebulous to this:
Is there a better way to create an SKShapeNode that is a line between two points without calling a convenience initializer? I plan on subclassing SKShapeNode to add additional logic to the node, and the subclass doesn't have access to convenience initializers of the superclass.
More importantly, how do I get the scene to recognize my line node there and trigger touchesEnded?
Is there a way, using that mechanism, I can make it a bit more "fuzzy", so it handles touches close to the line, instead of on the line? My eventual plan is to make it thinner, but I'd still like to have a large touch zone. I figure if nothing else, I can create two nodes: one clear/thick and the other red/thin, and handle touch events on the clear one, but I would like to know if there's a better way.
In answer to your main question, there is a problem in your touchesEnded method. The template shown by apple recommends the following layout:
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
}
}
You can then use this method to see if the location value is inside the lines frame (in order to do this, i created the "myNode" variable outside of the didMoveToView so that i could access it from the rest of the functions):
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(world)
if CGRectContainsPoint(myNode.frame, location) {
println("Touched \(myNode.name)")
} else {
println("Nothing here")
}
}
}
As for a "fuzzier" line, you can simply check a larger CGRect. In the code above, i check myNode.frame but you could create a variable with a CGRect which is slightly larger than the line so that you can detect touches which don't directly hit it.
As for more concise code, i cannot think of any at the moment but that isn't to say that there isn't a way. However, I don't quite understand what you mean about subclasses not having access to convenience methods as they can have access to whatever the superclass does so long as you import correctly.
I hope this helps.