I am implementing a custom UIGestureRecognizer subclass.
I would like to implement velocityInView: the same way that UIPanGestureRecognizer has done it. But I'm not sure how to go about doing it. How do I calculate the velocity in points / second?
Firstly, if you're using Swift, you're going to need to create a Bridging Header and #import <UIKit/UIGestureRecognizerSubclass.h> so you can override UIGestureRegoniser's touches began/moved/cancelled/ended methods. If you're using Objective-C just put that import statement in the .h or .m file.
Secondly, once you've created your UIGestureRecognizer subclass you need these variables in it:
private lazy var displayLink: CADisplayLink = {
let link = CADisplayLink(target: self, selector: Selector("timerCalled"))
link.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
return link
}()
private var previousTouchLocation: CGPoint?
private var currentTouchLocation : CGPoint?
private var previousTime: Double?
private var currentTime : Double?
The display link is for updating the currentTime and previousTime.
Thirdly, to continue setting everything up you need to override the following methods, I think it's all fairly self-explanatory:
override func touchesBegan(touches: Set<NSObject>!, withEvent event: UIEvent!) {
self.state = .Began
displayLink.paused = false
}
override func touchesMoved(touches: Set<NSObject>!, withEvent event: UIEvent!) {
if let view = self.view,
touch = touches.first as? UITouch {
self.state = .Changed
let newLocation = touch.locationInView(view)
self.previousTouchLocation = self.currentTouchLocation
self.currentTouchLocation = newLocation
}
}
override func touchesEnded(touches: Set<NSObject>!, withEvent event: UIEvent!) {
self.state = .Ended
displayLink.paused = true
}
override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!) {
self.state = .Cancelled
displayLink.paused = true
}
Fourthly, now we can get to the more interesting bit - calculating the velocity:
func velocityInView(targetView: UIView) -> CGPoint {
var velocity = CGPoint.zeroPoint
if let view = self.view,
prevTouchLocation = self.previousTouchLocation,
currTouchLocation = self.currentTouchLocation,
previousTime = self.previousTime,
currentTime = self.currentTime {
let targetPrevLocation = view.convertPoint(prevTouchLocation, toView: targetView)
let targetCurrLocation = view.convertPoint(currTouchLocation, toView: targetView)
let deltaTime = CGFloat(currentTime - previousTime)
let velocityX = (targetCurrLocation.x - targetPrevLocation.x) / deltaTime
let velocityY = (targetCurrLocation.y - targetPrevLocation.y) / deltaTime
velocity = CGPoint(x: velocityX, y: velocityY)
}
return velocity
}
The velocity is calculated using the equation:
velocity = deltaDistance / deltaTime
In this case, take each component of the velocity (x and y) and use that equation to calculate the velocity in each axis. If you wanted to combine both components of the velocity you would use Pythagoras' Theorem like so:
let distance = hypot(targetCurrLocation.x - targetPrevLocation.x,
targetCurrLocation.y - targetPrevLocation.y)
let velocity = distance / deltaTime
Fifthly, in your view controller you can now add your gesture recognizer and use action to get the velocity when dragging over the screen:
override func viewDidLoad() {
super.viewDidLoad()
let recognizer = VelocityGestureRecognizer(target: self, action: Selector("movedGesture:"))
self.view.addGestureRecognizer(recognizer)
}
func movedGesture(recognizer: VelocityGestureRecognizer) {
let v = recognizer.velocityInView(self.view)
println("velocity = \(v)")
}
Related
Is it possible to customize the area from the button at which it is considered .touchDragExit (or .touchDragEnter) (out of its selectable area?)?
To be more specific, I am speaking about this situation: I tap the UIButton, the .touchDown gets called, then I start dragging my finger away from the button and at some point (some distance away) it will not select anymore (and of course I can drag back in to select...). I would like the modify that distance...
Is this even possible?
You need to overwrite the UIButton continueTracking and touchesEnded functions.
Adapting #Dean's link, the implementation would be as following (swift 4.2):
class ViewController: UIViewController {
#IBOutlet weak var button: DragButton!
override func viewDidLoad() {
super.viewDidLoad()
}
}
class DragButton: UIButton {
private let _boundsExtension: CGFloat = 0 // Adjust this as needed
override open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension))
let currentLocation: CGPoint = touch.location(in: self)
let previousLocation: CGPoint = touch.previousLocation(in: self)
let touchOutside: Bool = !outerBounds.contains(currentLocation)
if touchOutside {
let previousTouchInside: Bool = outerBounds.contains(previousLocation)
if previousTouchInside {
print("touchDragExit")
sendActions(for: .touchDragExit)
} else {
print("touchDragOutside")
sendActions(for: .touchDragOutside)
}
} else {
let previousTouchOutside: Bool = !outerBounds.contains(previousLocation)
if previousTouchOutside {
print("touchDragEnter")
sendActions(for: .touchDragEnter)
} else {
print("touchDragInside")
sendActions(for: .touchDragInside)
}
}
return true
}
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let outerBounds: CGRect = bounds.insetBy(dx: CGFloat(-1 * _boundsExtension), dy: CGFloat(-1 * _boundsExtension))
let currentLocation: CGPoint = touch.location(in: self)
let touchInside: Bool = outerBounds.contains(currentLocation)
if touchInside {
print("touchUpInside action")
return sendActions(for: .touchUpInside)
} else {
print("touchUpOutside action")
return sendActions(for: .touchUpOutside)
}
}
}
Try changing the _boundsExtension value
The drag area is exaclty equal to the area define by bounds.
So if you want to customize the drag are simple customise the bounds of your button.
I have to buttons leftbutton, rightbutton both of them being SKSpriteNode().
When the user touches one of the buttons, there is a little ship that moves left and right as long as the user holds the touch.
Now I need a function or anything else that gives me the ship.position.x the whole time. I am stuck to try to make it print the position constantly. I can make it print everytime the button is touched but it only prints it once.
In my didMove I only created the buttons and the ship. So it should be rather irrelevant.
func moveShip (moveBy: CGFloat, forTheKey: String) {
let moveAction = SKAction.moveBy(x: moveBy, y: 0, duration: 0.09)
let repeatForEver = SKAction.repeatForever(moveAction)
let movingSequence = SKAction.sequence([moveAction, repeatForEver])
ship.run(movingSequence, withKey: forTheKey)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("\(ship.position.x)")
for touch: AnyObject in touches {
let pointTouched = touch.location(in: self)
if leftButton.contains(pointTouched) {
// !! I MAKE IT PRINT THE POSITION HERE !!
moveShip(moveBy: -30, forTheKey: "leftButton")
}
else if rightButton.contains(pointTouched) {
moveShip(moveBy: 30, forTheKey: "rightButton")
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let pos = touch.location(in: self)
let node = self.atPoint(pos)
if node == aButton {
} else {
ship.removeAction(forKey: "leftButton")
ship.removeAction(forKey: "rightButton")
}
}
}
In my code, the position is only printed once at the beginning of the touch and not printed until you release the touch and touch it again. Is there a possible way to do this with my code?
The touchesMoved function won't help you with your particular problem. You can check your frame constantly by creating a var timer = Timer() as instance variable.
You then have to set up the timer and the function that is called when a specific amount of time is over.
Do as following in your didMove function:
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self,
selector: #selector(detectShipPosition), userInfo: nil, repeats: true)
As this will repeat itself every 0.01 seconds it will call the function detectShipPosition which you will implement OUTSIDE didMove.
func detectShipPosition(){
print("\(ship.position.x)")
}
Hi you can make use of the touchesMoved delegate method(It tells the responder when one or more touches associated with an event changed.) as follows,
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
print("\(ship.position.x)")
}
Solution for the comment you posted on Bharath answer.
You can change below with your own value:
longGesture.minimumPressDuration
You can use UILongPressGestureRecognizer :
class ViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var leftButton: UIButton!
#IBOutlet weak var rightButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(ViewController.longPress(_:)))
longGesture.minimumPressDuration = 0.5
longGesture.delegate = self
leftButton.addGestureRecognizer(longGesture)
rightButton.addGestureRecognizer(longGesture)
}
#objc func longPress(_ gestureReconizer: UILongPressGestureRecognizer) {
print("\(ship.position.x)")
}
}
If you implement the update() function in your scene, it will be called for every frame and you can check your object's position (and existence) in there to print the value when it changes:
override func update(_ currentTime: TimeInterval)
{
if let ship = theShipSprite,
ship.position != lastPosition
{
print(ship.position) // or whatever it is you need to do
lastPosition = shipPosition
}
}
I am making a game for iOS in Swift with SpriteKit, the game is currently just a ball moving around with a sword.
I have anchored the sword to the bottom of the sword, but I need to know how to control the direction of rotation with 2 buttons or a slider of some sort.
Here is my code:
import SpriteKit
let sprite = SKSpriteNode(imageNamed:"Player")
let weapon = SKSpriteNode(imageNamed: "weapon")
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let initialPlayerLocation = CGPoint(x: self.frame.width/2, y: self.frame.height/2)
/* Setup your scene here */
//player
sprite.setScale(1.0)
sprite.position = initialPlayerLocation
sprite.zPosition = 20
self.addChild(sprite)
//weapon
weapon.setScale(1.0)
weapon.position = initialPlayerLocation
weapon.zPosition = -20
weapon.anchorPoint = CGPointMake(0.5,0.0);
self.addChild(weapon)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
var move = SKAction.moveTo(location, duration:1.0)
sprite.runAction(move)
weapon.runAction(move)
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
Okay, I think I understand what you are trying to do. It involves a lot of code, but in theory it is pretty simple. We detect if the touch is inside the left or right buttons (which in this example, I've made them SKSpriteNodes), and then set boolean values for the update method to use and rotate the weapon node (I'm assuming this is the sword you were talking about).
I've also included a variable that lets you set the speed of rotation. As you did not say what speed you were looking for, I left it up to you.
At the top of the program:
let sprite = SKSpriteNode(imageNamed:"Player")
let weapon = SKSpriteNode(imageNamed: "weapon")
let leftButton = SKSpriteNode(imageNamed: "leftButton")
let rightButton = SKSpriteNode(imageNamed: "rightButton")
var leftPressed = false
var rightPressed = false
let weaponRotateSpeed = 0.01 // Change this for rotation speed of weapon
And then modify touchesBegan to look like this:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
if (leftButton.containsPoint(p: location))
leftPressed = true
else if (rightButton.containsPoint(p: location))
rightPressed = true
else {
var move = SKAction.moveTo(location, duration:1.0)
sprite.runAction(move)
weapon.runAction(move)
}
}
In update, add this code:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if (leftPressed && rightPressed) {
// Do nothing, as both buttons are pressed
} else if (leftPressed) {
weapon.zRotation -= weaponRotateSpeed
} else if (rightPressed) {
weapon.zRotation += weaponRotateSpeed
}
}
We want to stop the weapon from rotating when we detect when the finger has moved off the button like so:
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
if (leftButton.containsPoint(p: location))
leftPressed = true
else
leftPressed = false
if (rightButton.containsPoint(p: location))
rightPressed = true
else
rightPressed = false
}
}
And at last, we need to detect when your finger has left the screen:
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if (leftButton.containsPoint(p: location))
leftPressed = false
if (rightButton.containsPoint(p: location))
rightPressed = false
}
}
I want to drag textField around my view. but im having small problem with my code.
here my code
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
var touch : UITouch! = touches.first as! UITouch
location = touch.locationInView(self.view)
bottomTextField.center = location
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
var touch : UITouch! = touches.first as! UITouch
location = touch.locationInView(self.view)
bottomTextField.center = location
}
Problem is if i tried to touch on the TextField and drag then it doesn't work. but if i just touch somewhere else and drag my finger then TextField start to drag. i want t make it only if i touch on that textFiel.
You can achieve this by adding a gesture to the text field:
override func viewDidLoad() {
super.viewDidLoad()
var gesture = UIPanGestureRecognizer(target: self, action: Selector("userDragged:"))
bottomTextField.addGestureRecognizer(gesture)
bottomTextField.userInteractionEnabled = true
}
func userDragged(gesture: UIPanGestureRecognizer){
var loc = gesture.locationInView(self.view)
self.bottomTextField.center = loc
}
If it helps anyone, following is an updated working version of the accepted answer for swift 4.0 and 5.0
override func viewDidLoad() {
super.viewDidLoad()
//Draggable textfields
let gesture = UIPanGestureRecognizer(target: self, action: #selector(userDragged(gesture:)))
bottomTextView.addGestureRecognizer(gesture)
bottomTextView.isUserInteractionEnabled = true
}
#objc func userDragged(gesture: UIPanGestureRecognizer){
let loc = gesture.location(in: self.view)
self.bottomTextView.center = loc
}
So I have a pretty basic slingshot that has two physics bodies attached to one another using a SKPhysicsSpringJoint. When touches begin, I create a projectile and spring. When touches are moved, I move the projectile accordingly to the user's touch position. When touches end, the projectile is launched.
The issue is whenever the user stops moving the projectile, but doesn't let go, the projectile moves towards its anchor point and swings around it until the user releases their touch. I've been working for hours trying to figure out why this is happening, any ideas?
Here are the main functions:
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let touchLocation = touch.locationInNode(self)
//Add Cannon Ball
var uniqueID:Int = nodeArray.count
var uniqueString = "cannonBall\(uniqueID)"
var cannonBall = CannonBallNode(location: CGPoint(x: size.width/2, y: 200))
cannonBall.name = uniqueString
// 3 - Determine offset of location to projectile
let offset = touchLocation - cannonBall.position
// 4 - Bail out if you click above cannon
if (offset.y > 50) { return }
self.addChild(cannonBall)
println("\(nodeArray.count)")
if var cannon = childNodeWithName("cannon") {
if var heldCannonBall = childNodeWithName(uniqueString) {
heldCannonBall.physicsBody?.affectedByGravity = false
var spring = SKPhysicsJointSpring.jointWithBodyA(cannon.physicsBody, bodyB: cannonBall.physicsBody, anchorA: cannon.position, anchorB: cannonBall.position)
spring.damping = 0.4;
spring.frequency = 1.0;
// Add Physics
physicsWorld.addJoint(spring)
}
}
}
}
override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let touchLocation = touch.locationInNode(self)
var uniqueID:Int = nodeArray.count
var uniqueString = "cannonBall\(uniqueID)"
if var heldCannonBall = childNodeWithName(uniqueString) {
// 3 - Determine offset of location to projectile
let offset = touchLocation - heldCannonBall.position
// 4 - Bail out if you are shooting down or backwards
if (offset.y > 0) { return }
heldCannonBall.position = touchLocation
}
}
}
override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let touchLocation = touch.locationInNode(self)
var uniqueID:Int = nodeArray.count
var uniqueString = "cannonBall\(uniqueID)"
if var releasedCannonBall = childNodeWithName(uniqueString) {
var angle = CGFloat(M_PI_4)
var magnitude:CGFloat = 1;
releasedCannonBall.physicsBody?.applyImpulse(CGVectorMake(magnitude*cos(angle), magnitude*sin(angle)))
releasedCannonBall.physicsBody?.affectedByGravity = true
}
runAction(SKAction.sequence([
SKAction.runBlock{[self.physicsWorld.removeAllJoints()]},
SKAction.waitForDuration(0.1)]
))
nodeArray.append("\(uniqueString)")
}
}
Finally came across the solution! I needed to produce an update function to update the projectile's position as it was moved via the touchesMoved function. I created a global variable and had it store the location of the touch when it began and when it was moved. I threw it in the update function and voila, no more issues.
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
//#1
var uniqueID:Int = nodeArray.count
var uniqueString = "cannonBall\(uniqueID)"
println("\(cannonBallLocation)")
if var releasedCannonBall = childNodeWithName(uniqueString) {
releasedCannonBall.position = cannonBallLocation
}
} //end func