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.
Related
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 )
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
}
}
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.
i have two functions, one is activated when i touch the scene and another one when i make a gesture down, but when i make a gesture the scene detect a touch and execute both functions
class GameScene: SKScene, SKPhysicsContactDelegate {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
// physics & collisions
physicsWorld.gravity = CGVectorMake(0.0, 0.0)
physicsWorld.contactDelegate = self
// Swipe down
let swipeDown:UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: Selector("swipedDown:"))
swipeDown.direction = .Down
view.addGestureRecognizer(swipeDown)
}
//MARK: Swipes
func swipedDown(sender:UISwipeGestureRecognizer){
//first function
swipeDown()
}
//MARK: Touches
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
// second function
attack()
}
}
}
Instead of touchesBegan, try using a tap gesture (UITapGestureRecognizer) for attack action.
This should not be necessary but if you still get conflict between swipe and tap, make the tap gesture depend on swipe's failure using requireGestureRecognizerToFail to ensure that a tap is not detected during a swipe.
You could try something like this
var swiped = Bool()
func swipedDown(sender:UISwipeGestureRecognizer){
//first function
swipeDown()
swiped = true
}
Override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
// second function
if swiped == false {
attack()
}
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch in touches {
swiped = false
}
}
Hope this helps.
I want to scroll a node in sprite kit without uiscrollview
// In touchesMoved
let touch: UITouch = touches.first as! UITouch;
let location:CGPoint = touch.locationInNode(self);
let lastPosition = touch.previousLocationInNode(self)
var newLoc = selectNode.position.y + (location.y - lastPosition.y)
// Check if the top part is reached
if(newLoc < selectNodeStart){
newLoc = selectNodeStart
}
if(selectNode.position.y >= selectNodeStart){
// let sk = SKAction.moveToY(newLoc, duration: 0.1)
// selectNode.runAction(sk)
selectNode.position.y = newLoc
}
If I use the sk action the result is very horrible, but without the result is also horrible
An Elegant way of doing this in SpriteKit is using a UIPanGestureRecognizer to detect the touch, and inside of that you will create a sequence of SKActions that will accelerate or decelerate the movement. You might have to use the update method and/or touches to handle the pan stopping or another immediately starting. Unfortunately, if you want to use only SpriteKit you will have to use SKActions to implement this.
I thought I'd also mention an easier (and possible better looking) way to do this. Take a look at "ScrollKit", which works pretty well (although it does use a UIScrollView): https://github.com/bobmoff/ScrollKit
If you found that the UIScrollView doesn't pass touches up to touchesMoved, you could try a few different things.
- You can add a UITapGestureRecognizer and set on the UIScrollView CanCancelContentTouches to YES.
- You can subclass UIScrollView and override the touchesMethods so that if the user isn't scrolling, touch information will be passed up the responder chain:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent){
if (!self.dragging){
self.nextResponder.touchesBegan(touches , withEvent: event)
}
else{
super.touchesBegan(touches , withEvent: event)
}
}
override func touchesMoved(touches: NSSet, withEvent event: UIEvent){
if (!self.dragging){
self.nextResponder.touchesMoved(touches , withEvent:event)
}
else{
super.touchesMoved(touches , withEvent: event)
}
}
override func touchesEnded(touches: NSSet, withEvent: event UIEvent){
if (!self.dragging){
self.nextResponder.touchesEnded(touches , withEvent: event)
}
else{
super.touchesEnded(touches , withEvent: event)
}
}