Simultaneous gestures and scaling of pinch Gesture in swift - swift

Hopeing for a bit of enlightenment. I have a piece of code. Which is working fairly OK.
I have 2 gestures implemented - pinch and rotate.
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinchAction(sender:)))
view.addGestureRecognizer(pinchGesture)
pinchGesture.delegate = self
let rotateGesture = UIRotationGestureRecognizer(target: self, action: #selector(rotateAction(sender:)))
view.addGestureRecognizer(rotateGesture)
rotateGesture.delegate = self
and then this function which I found here on another thread.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UIRotationGestureRecognizer) {
return true
} else {
return false
}
}
my class is set to
class GameScene: SKScene, UIGestureRecognizerDelegate {
so my first question is. How the heck is this function actually being called? Is it automatically called because it's called gestureRecognizer? I don't call it elsewhere as a function in my code. Sorry just find it confusing.
the other interesting issue is that as I rescale my SKSprite node with the pinch function I want it to not jump back to the scale of the initial pinch. I have set a variable for the scale of the node being adjusted and take that in the pinch began and also adjust the variable when pinch ends as so:
#objc func pinchAction(sender:UIPinchGestureRecognizer){
if sender.state == .began{
//print("Pinch Began")
pointer.setScale(CGFloat(protractorScale * 0.5))
protractor.setScale(CGFloat(protractorScale * 0.45))
}
if sender.state == .changed{
// print("Pinch Change")
let newScale = sender.scale
protractorScale = Double(newScale)
print(newScale)
pointer.setScale(CGFloat(protractorScale * 0.5))
protractor.setScale(CGFloat(protractorScale * 0.45))
ballSpeed = Float(newScale * 500)
}
if sender.state == .ended{
// print("Pinch Ended")
let newScale = sender.scale
protractorScale = Double(newScale)
}
}
but it still jumps to whatever pinch position I start with while it ought to stay stable between individual pinches rather than reseting.
I appreciate my code is simplistic so am also open to suggestions for a tighter code. This is my first attempt at using simultaneous gestures.Thanks

I finally understood that obviously the pinch gesture always initially returns a pinch scale factor of 1. Obviously! Once I realised that and after KnightOfDragon's advice it was straightforward.
#objc func pinchAction(sender:UIPinchGestureRecognizer){
if sender.state == .began{
let startScale = sender.scale
print("initial pinch scale \(startScale)")
pointer.setScale(CGFloat(protractorScale))
protractor.setScale(CGFloat(protractorScale))
}
if sender.state == .changed{
let getScale = sender.scale
print(getScale)
let newScale = (CGFloat(protractorScale) * getScale)
pointer.setScale(newScale)
protractor.setScale(newScale)
ballSpeed = Float(newScale * 100)
ballSpeedIndicator.text = "\(Int(ballSpeed))"
}
if sender.state == .ended{
// print("Pinch Ended")
let newScale = sender.scale
protractorScale = protractorScale * Double(newScale)
}
}
I set my initial node scale also to 1 as a global variable. So now in the state ended I return that * the pinch scale and the scaling now remains perfectly stable.

Related

How do I zoom the view when the player zooms in or out?

So I just learned how to make a view zoom in and out, and I did it for my gamescene but theres just a black box right outside the view, like this
enter image description here
Here's my code that I used to allow zooming in and out, this is in my didMove function
let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(sender:)))
view.addGestureRecognizer(pinch)
and this is inside no function, just the GameScene class
#objc func handlePinch(sender: UIPinchGestureRecognizer) {
guard sender.view != nil else { return }
if sender.state == .began || sender.state == .changed {
sender.view?.transform = (sender.view?.transform.scaledBy(x: sender.scale, y: sender.scale))!
sender.scale = 1.0
}
}
How would I change the view whenever someone zooms in or out?

UIPanGestureRecognizer is not working in iOS 13

We have developed an app in iOS 12 which worked really fine. Now in iOS 13 UIPanGestureRecognizer is not working any more.
I've search for solutions but didn't find anything.
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
if let view = recognizer.view {
let center = view.frame.origin.y + view.bounds.height / 2
if(center <= SliderView.bounds.height && center >= SliderView.bounds.minY) {
view.center = CGPoint(x:view.center.x, y:view.center.y + translation.y)
}
if(center > SliderView.bounds.height) {
view.center = CGPoint(x: view.center.x, y: view.center.y - 1)
}
if(center < SliderView.bounds.minY) {
view.center = CGPoint(x: view.center.x, y: view.center.y + 1)
}
lowerSliderView.frame = CGRect(x: 0, y: center, width: SliderView.bounds.width, height: SliderView.bounds.height - center)
slider = 1 - Float(center / SliderView.bounds.height)
slider = min(slider, 1.0)
slider = max(slider, 0.0)
}
recognizer.setTranslation(CGPoint.zero, in: self.view)
}
I expect that the slider will work on the app.
I had similar issues with my gesture recognizer on iOS13. (Worked fine on 12)
My problem was: I was setting .center = someValue, on my view which had the gesture recognizer on it, but that view also had Constraints on it. iOS13 doesn't seem to like it when you have constraints on a view, and are also setting it's frame manually. So i converted my code completely to just set the frame inside the gesture recognizer's handler method. iOS13 seems to have gotten more strict with preventing you from setting .center, or .frame manually if you also have constraints on that view and are calling layoutIfNeeded() causing a layout pass. I'm not sure about this, but for now, i'm back up and running using manual frame setting.
If your gesture recognizer event is not firing, please try to implement the following method and inspect the values inside to see if there are other gesture recognizers overlaid and competing for the touch gesture. Return TRUE for your gesture recognizer, and suppress others, or just return true for all of them. You need to set the delegate first.
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UIRotationGestureRecognizer) {
print("Should recognize")
return true
} else {
print("Should NOT recognize")
return false
}
}
This is the setup of mine. works fine now, after I removed all the constraints from my view which had the recognizer on it. Constraints were "undoing" the translations which I had in the gesture recognizer method, only resulting in +1 or -1 movement of the view, and then it snapped back in place.
let recStart = UIPanGestureRecognizer(target: self, action: #selector(handleStartPan))
recStart.delegate = self
self.startView.addGestureRecognizer(recStart)
And handleStartPan:
#objc final func handleStartPan(_ gestureRecognizer: UIPanGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
let translation = gestureRecognizer.translation(in: containerOfMyView)
// do stuff with translation.x or .y
...

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

(Swift) pan & zoom constrained to a certain size image

I am trying to pan and zoom across an image background in spritekit, I have managed to get the zoom working ok and manually entered some restrictions on how far you can pan the image, however the problem is when you pan the screen right to the edge of the image and then zoom out the background shows.
I want the camera to restrict only to the image on screen and not any blank background. Any ideas on how I should do this or any better solutions?
Here is what I got so far
class GameScene:SKScene{
var cam: SKCameraNode!
var scaleNum:CGFloat=1
override func didMove(to view: SKView){
cam=SKCameraNode()
cam.setScale(CGFloat(scaleNum))
self.camera=cam
self.addChild(cam)
let gesture=UIPinchGestureRecognizer(target: self, action: #selector(zoomIn(recognizer:)))
self.view!.addGestureRecognizer(gesture)
}
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed{
cam.setScale(recognizer.scale)
scaleNum=recognizer.scale
if cam.xScale<1 || cam.yScale<1{
cam.setScale(1)
}
if cam.xScale>3 || cam.yScale > 3{
cam.setScale(3)
}
// recognizer.scale=1
test()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let firstTouch=touches.first
let location=(firstTouch?.location(in: self))!
let previousLocation=firstTouch?.previousLocation(in: self)
cam?.position.x -= location.x - (previousLocation?.x)!
cam?.position.y -= location.y - (previousLocation?.y)!
test()
}
func test(){
if cam.position.x < 1000*scaleNum{
cam.position.x=1000*scaleNum
}
if cam.position.x > 9200*scaleNum{
cam.position.x=9200*scaleNum
}
if cam.position.y<617*scaleNum{
cam.position.y=617*scaleNum
}
if cam.position.y>4476*scaleNum{
cam.position.y=4476*scaleNum
}
}
}
First of all, I would change your zoomIn function to this:
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed {
scaleNum = recognizer.scale
if scaleNum < 1 { scaleNum = 1 }
if scaleNum > 3 { scaleNum = 3 }
cam.setScale(scaleNum)
test()
}
}
It is easier to understand, you're not setting the camera scale twice, and most importantly, when you clamp the camera scale, scaleNum reflects that clamped value. This was not the case before, and in fact, that small change might be your entire problem.
Now I don't have much experience with UIPinchGestureRecognizer but I think the reason your zoom gesture works "ok" is because you are assigning directly from recognizer.scale to cam scale. Correct me if I'm wrong, but I think UIGestureRecognizer always starts with a scale of 1 for each new gesture, but your camera scale maintains its last value.
As an example, imagine your camera is at a scale of 1. A user zooms in to a scale of 2, the scene zooms in perfectly. The user lifts their fingers ending the gesture. Then the user tries to zoom in more, so they begin a new gesture, starting with a scale of 1, but your scene is still at a scale of 2. You can't assign the gesture scale directly or the image scale will 'jump' back to 1 for each new gesture. You have to convert from the gesture scale space to the camera scale space.
How exactly you do this is a design and feel choice. With no experience, my advice would be to change the line in my zoomIn function from
`scaleNum = recognizer.scale'
to
`scaleNum *= recognizer.scale`
Try both versions, and let me know how they work. If there is still a problem, then it most likely resides in your test() function. If so, I will try and help out with that as needed.
Thanks for the answer above, I managed to get it working, code below. Still needs a bit of tweaking but you can pan and zoom anywhere on the background image but the view should be constrained within the background image and not move into empty space beyond the image
import SpriteKit
import GameplayKit
class GameScene: SKScene {
var cam: SKCameraNode!
var scaleNum: CGFloat=1
var background: SKSpriteNode!
var playableRect: CGRect!
override func didMove(to view: SKView) {
background=self.childNode(withName: "clouds") as! SKSpriteNode
cam=SKCameraNode()
cam.setScale(CGFloat(scaleNum))
self.camera=cam
self.addChild(cam)
self.isUserInteractionEnabled=true
let gesture=UIPinchGestureRecognizer(target: self, action: #selector(zoomIn(recognizer:)))
self.view!.addGestureRecognizer(gesture)
let maxAspectRatio:CGFloat=16.0/9.0
let playableHeight=size.width/maxAspectRatio
let playableMargin=(size.height-playableHeight)/2.0
playableRect=CGRect(x:0, y: playableMargin, width: size.width, height: playableHeight)
}
func zoomIn(recognizer: UIPinchGestureRecognizer){
if recognizer.state == .changed{
let savedScale=scaleNum
scaleNum=recognizer.scale
if scaleNum<1{
scaleNum=1
}
else if scaleNum>3{
scaleNum=3
}
if testcamera(posX: cam.position.x, posY: cam.position.y){
cam.setScale(scaleNum)
}
else{
scaleNum=savedScale
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let firstTouch=touches.first
let location=(firstTouch?.location(in: self))!
var posX=cam.position.x
var posY=cam.position.y
let previousLocation=firstTouch?.previousLocation(in: self)
posX -= location.x - (previousLocation?.x)!
posY -= location.y - (previousLocation?.y)!
if testcamera(posX: posX, posY: posY){
cam.position.x=posX
cam.position.y=posY
}
}
func testcamera(posX: CGFloat, posY: CGFloat)->Bool{
var cameraRect : CGRect {
let xx = posX - size.width/2*scaleNum
let yy = posY - playableRect.height/2*scaleNum
return CGRect(x: xx, y: yy, width: size.width*scaleNum, height: playableRect.height*scaleNum)
}
let backGroundRect=CGRect(x: background.position.x-background.frame.width/2, y: background.position.y-background.frame.height/2, width: background.frame.width, height: background.frame.height)
return backGroundRect.contains(cameraRect)
}
}

How can I capture which direction is being panned using UIPanGestureRecognizer?

Ok so I have been looking around at just about every option under the sun for capturing multi-touch gestures, and I have finally come full circle and am back at the UIPanGestureRecognizer.
The functionality I want is really quite simple. I have setup a two finger pan gesture, and I want to be able to shuffle through some images depending on how many pixels I move. I have all that worked out okay, but I want to be able to capture if the pan gesture is REVERSED.
Is there a built in way that I'm just not seeing to detect going back on a gesture? Would I have to store my original starting point, then track the end point, then see where they move after that and se if its less than the initial ending point and then reverse accordingly? I can see that working, but I'm hoping there is a more elegant solution!!
Thanks
EDIT:
Here is the method that the recognizer is set to fire. Its a bit of a hack, but it works:
-(void) throttle:(UIGestureRecognizer *) recognize{
throttleCounter ++;
if(throttleCounter == 6){
throttleCounter = 0;
[self nextPic:nil];
}
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *) recognize;
UIView *view = recognize.view;
if(panGesture.state == UIGestureRecognizerStateBegan){
CGPoint translation = [panGesture translationInView:view.superview];
NSLog(#"X: %f, Y:%f", translation.x, translation.y);
}else if(panGesture.state == UIGestureRecognizerStateEnded){
CGPoint translation = [panGesture translationInView:view.superview];
NSLog(#"X: %f, Y:%f", translation.x, translation.y);
}
}
I've just gotten to the point where I am going to start trying to track the differences between values...to try and tell which way they are panning
On UIPanGestureRecognizer you can use -velocityInView: to get the velocity of the fingers at the time that gesture was recognised.
If you wanted to do one thing on a pan right and one thing on a pan left, for example, you could do something like:
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer
{
CGPoint velocity = [gestureRecognizer velocityInView:yourView];
if(velocity.x > 0)
{
NSLog(#"gesture went right");
}
else
{
NSLog(#"gesture went left");
}
}
If you literally want to detect a reversal, as in you want to compare a new velocity to an old one and see if it is just in the opposite direction — whichever direction that may be — you could do:
// assuming lastGestureVelocity is a class variable...
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecognizer
{
CGPoint velocity = [gestureRecognizer velocityInView:yourView];
if(velocity.x*lastGestureVelocity.x + velocity.y*lastGestureVelocity.y > 0)
{
NSLog(#"gesture went in the same direction");
}
else
{
NSLog(#"gesture went in the opposite direction");
}
lastGestureVelocity = velocity;
}
The multiply and add thing may look a little odd. It's actually a dot product, but rest assured it'll be a positive number if the gestures are in the same direction, going down to 0 if they're exactly at right angles and then becoming a negative number if they're in the opposite direction.
Here's an easy to detect before the gesture recognizer begins:
public override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let panRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
// Ensure it's a horizontal drag
let velocity = panRecognizer.velocity(in: self)
if abs(velocity.y) > abs(velocity.x) {
return false
}
return true
}
If you want a vertical only drag, you can switch the x and y.
This code from Serghei Catraniuc worked out better for me.
https://github.com/serp1412/LazyTransitions
func addPanGestureRecognizers() {
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(respondToSwipeGesture(gesture:)))
self.view.addGestureRecognizer(panGesture)
}
func respondToSwipeGesture(gesture: UIGestureRecognizer){
if let swipeGesture = gesture as? UIPanGestureRecognizer{
switch gesture.state {
case .began:
print("began")
case .ended:
print("ended")
switch swipeGesture.direction{
case .rightToLeft:
print("rightToLeft")
case .leftToRight:
print("leftToRight")
case .topToBottom:
print("topToBottom")
case .bottomToTop:
print("bottomToTop")
default:
print("default")
}
default: break
}
}
}
// Extensions
import Foundation
import UIKit
public enum UIPanGestureRecognizerDirection {
case undefined
case bottomToTop
case topToBottom
case rightToLeft
case leftToRight
}
public enum TransitionOrientation {
case unknown
case topToBottom
case bottomToTop
case leftToRight
case rightToLeft
}
extension UIPanGestureRecognizer {
public var direction: UIPanGestureRecognizerDirection {
let velocity = self.velocity(in: view)
let isVertical = fabs(velocity.y) > fabs(velocity.x)
var direction: UIPanGestureRecognizerDirection
if isVertical {
direction = velocity.y > 0 ? .topToBottom : .bottomToTop
} else {
direction = velocity.x > 0 ? .leftToRight : .rightToLeft
}
return direction
}
public func isQuickSwipe(for orientation: TransitionOrientation) -> Bool {
let velocity = self.velocity(in: view)
return isQuickSwipeForVelocity(velocity, for: orientation)
}
private func isQuickSwipeForVelocity(_ velocity: CGPoint, for orientation: TransitionOrientation) -> Bool {
switch orientation {
case .unknown : return false
case .topToBottom : return velocity.y > 1000
case .bottomToTop : return velocity.y < -1000
case .leftToRight : return velocity.x > 1000
case .rightToLeft : return velocity.x < -1000
}
}
}
extension UIPanGestureRecognizer {
typealias GestureHandlingTuple = (gesture: UIPanGestureRecognizer? , handle: (UIPanGestureRecognizer) -> ())
fileprivate static var handlers = [GestureHandlingTuple]()
public convenience init(gestureHandle: #escaping (UIPanGestureRecognizer) -> ()) {
self.init()
UIPanGestureRecognizer.cleanup()
set(gestureHandle: gestureHandle)
}
public func set(gestureHandle: #escaping (UIPanGestureRecognizer) -> ()) {
weak var weakSelf = self
let tuple = (weakSelf, gestureHandle)
UIPanGestureRecognizer.handlers.append(tuple)
addTarget(self, action: #selector(handleGesture))
}
fileprivate static func cleanup() {
handlers = handlers.filter { $0.0?.view != nil }
}
#objc private func handleGesture(_ gesture: UIPanGestureRecognizer) {
let handleTuples = UIPanGestureRecognizer.handlers.filter{ $0.gesture === self }
handleTuples.forEach { $0.handle(gesture)}
}
}
extension UIPanGestureRecognizerDirection {
public var orientation: TransitionOrientation {
switch self {
case .rightToLeft: return .rightToLeft
case .leftToRight: return .leftToRight
case .bottomToTop: return .bottomToTop
case .topToBottom: return .topToBottom
default: return .unknown
}
}
}
extension UIPanGestureRecognizerDirection {
public var isHorizontal: Bool {
switch self {
case .rightToLeft, .leftToRight:
return true
default:
return false
}
}
}