Rotating SCNNode on Y Axis Based on Pan Gesture - ios

I have a model which I rotate using the pan gesture. It works fine but it does not rotate all 360 degrees. It rotates to 180 and then stops. Also if I end the pan gesture and starts again then the position is reset. Here is my code.
#objc func panned(recognizer :UIPanGestureRecognizer) {
var newAngleY :Float = 0.0
if recognizer.state == .changed {
let sceneView = recognizer.view as! ARSCNView
let touchPoint = recognizer.location(in: sceneView)
let translation = recognizer.translation(in: sceneView)
print(translation.x)
let scnHitTestResults = self.sceneView.hitTest(touchPoint, options: nil)
if let hitTestResult = scnHitTestResults.first {
let chairNode = hitTestResult.node
newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180
newAngleY += chairNode.eulerAngles.y/180
chairNode.eulerAngles.y = newAngleY
}
}
else if recognizer.state == .ended {
currentAngleY = newAngleY
}
}

You could try this:
Create a var currentAngleY: Float = 0.0
Then in your PanGestureRecognizer try the following:
/// Rotates An Object On It's YAxis
///
/// - Parameter gesture: UIPanGestureRecognizer
#objc func rotateObject(gesture: UIPanGestureRecognizer) {
guard let nodeToRotate = currentNode else { return }
let translation = gesture.translation(in: gesture.view!)
var newAngleY = (Float)(translation.x)*(Float)(Double.pi)/180.0
newAngleY += currentAngleY
nodeToRotate.eulerAngles.y = newAngleY
if(gesture.state == .ended) { currentAngleY = newAngleY }
}
nodeToRotate refers to an SCNNode I have already selected, but you should be able to adapt it as you see fit.

Related

UIPanGestureRecognizer to rotate mapView

I have a method for panning the map using a UIPanGestureRecognizer however it doesn't rotate correctly (I want to be able to make circle motions that cause the camera to move in a circle), is there a better approach here?
#objc func panMap (sender: UIPanGestureRecognizer) {
if sender.state == .began {
print("began")
} else if sender.state == .changed {
// rotating map camera
let position = sender.translation(in: mapView)
let newDirection = mapView.camera.heading.advanced(by: (Double(position.x) + Double(position.y)))
let newCamera: MKMapCamera = MKMapCamera(lookingAtCenter: mapView.camera.centerCoordinate, fromDistance: mapView.camera.altitude, pitch: mapView.camera.pitch, heading: newDirection)
mapView.setCamera(newCamera, animated: false)
// Eugene Dudnyk's solution to help fix logic error when appending distance
sender.setTranslation(.zero, in: mapView)
} else if sender.state == .ended {
print("end")
}
}
The issue is that translation of pan gesture recognizer accumulates relative to the position at the start of panning, but you interpret it as accumulation since the last state change, and always add to the camera angle.
In case if user moved finger to afa.x = 160, and then moves finger back, afa.x changes let's say from 160 to 150, but you will append to the angle 150 / 30 = 5 degrees where angle has to be negative, - 10 / 30 degrees instead.
Try this, maybe it will make things better:
#objc func panMap (sender: UIPanGestureRecognizer) {
if sender.state == .began {
print("began")
} else if sender.state == .changed {
// rotating map camera
let translation = sender.translation(in: mapView)
let location = sender.location(in: mapView)
let bounds = mapView.bounds
let vector1 = CGVector(dx: location.x - bounds.midX, dy: location.y - bounds.midY)
let vector2 = CGVector(dx: vector1.dx + translation.x, dy: vector1.dy + translation.y)
let angle1 = atan2(vector1.dx, vector1.dy)
let angle2 = atan2(vector2.dx, vector2.dy)
mapView.camera.heading += (angle2 - angle1) * 180.0 / Double.pi
} else if sender.state == .ended {
print("end")
}
sender.setTranslation(.zero, in: mapView)
}

Running swift condition just once on translation

I created a UIView and a UIImageView which is inside the UIView as a subview, then I added a pan gesture to the UIImageView to slide within the UIView, the image slides now but the problem I have now is when the slider gets to the end of the view if movex > xMax, I want to print this just once print("SWIPPERD movex"). The current code I have there continues to print print("SWIPPERD movex") as long as the user does not remove his/her hand from the UIImageView which is used to slide
private func swipeFunc() {
let swipeGesture = UIPanGestureRecognizer(target: self, action: #selector(acknowledgeSwiped(sender:)))
sliderImage.addGestureRecognizer(swipeGesture)
swipeGesture.delegate = self as? UIGestureRecognizerDelegate
}
#objc func acknowledgeSwiped(sender: UIPanGestureRecognizer) {
if let sliderView = sender.view {
let translation = sender.translation(in: self.baseView) //self.sliderView
switch sender.state {
case .began:
startingFrame = sliderImage.frame
viewCenter = baseView.center
fallthrough
case .changed:
if let startFrame = startingFrame {
var movex = translation.x
if movex < -startFrame.origin.x {
movex = -startFrame.origin.x
print("SWIPPERD minmax")
}
let xMax = self.baseView.frame.width - startFrame.origin.x - startFrame.width - 15 //self.sliderView
if movex > xMax {
movex = xMax
print("SWIPPERD movex")
}
var movey = translation.y
if movey < -startFrame.origin.y { movey = -startFrame.origin.y }
let yMax = self.baseView.frame.height - startFrame.origin.y - startFrame.height //self.sliderView
if movey > yMax {
movey = yMax
// print("SWIPPERD min")
}
sliderView.transform = CGAffineTransform(translationX: movex, y: movey)
}
default: // .ended and others:
UIView.animate(withDuration: 0.1, animations: {
sliderView.transform = CGAffineTransform.identity
})
}
}
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return sliderImage.frame.contains(point)
}
You may want to use the .ended state instead of .changed state, based on your requirements. And you've mentioned you want to get the right direction only. You could try below to determine if the swipe came from right to left, or vice-versa, change as you wish:
let velocity = sender.velocity(in: sender.view)
let rightToLeftSwipe = velocity.x < 0

Scaling a SCNNode in ARKit

I am trying to scale a 3D model of a chair in ARKit using SceneKit. Here is my code for the pinch gesture:
#objc func pinched(recognizer :UIPinchGestureRecognizer) {
var deltaScale :CGFloat = 0.0
deltaScale = 1 - self.lastScale - recognizer.scale
print(recognizer.scale)
let sceneView = recognizer.view as! ARSCNView
let touchPoint = recognizer.location(in: sceneView)
let scnHitTestResults = self.sceneView.hitTest(touchPoint, options: nil)
if let hitTestResult = scnHitTestResults.first {
let chairNode = hitTestResult.node
chairNode.scale = SCNVector3(deltaScale,deltaScale,deltaScale)
self.lastScale = recognizer.scale
}
}
It does scale but for some weird reason it inverts the 3D model upside down. Any ideas why? Also although the scaling works but it is not as smooth and kinda jumps from different scale factors when used in multiple progressions using pinch to zoom.
Here is how I scale my nodes:
/// Scales An SCNNode
///
/// - Parameter gesture: UIPinchGestureRecognizer
#objc func scaleObject(gesture: UIPinchGestureRecognizer) {
let location = gesture.location(in: sceneView)
let hitTestResults = sceneView.hitTest(location)
guard let nodeToScale = hitTestResults.first?.node else {
return
}
if gesture.state == .changed {
let pinchScaleX: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.x))
let pinchScaleY: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.y))
let pinchScaleZ: CGFloat = gesture.scale * CGFloat((nodeToScale.scale.z))
nodeToScale.scale = SCNVector3Make(Float(pinchScaleX), Float(pinchScaleY), Float(pinchScaleZ))
gesture.scale = 1
}
if gesture.state == .ended { }
}
In my example current node refers to an SCNNode, although you can set this however you like.

UIKit Dynamics with pan, rotate and pinch gesture

I have added UIDynamics to imageview and used pan gesture for that. It is working fine with pan gesture but when I apply pinch gesture with that it is not working. It is showing large imageview but when I start dragging then it is changed to original size.
Here is my code:
func handleAttachmentGesture(_ sender: UIPanGestureRecognizer) {
let location = sender.location(in: emojiSuperView!)
let boxLocation = sender.location(in: self)
switch sender.state {
case .began:
print("Your touch start position is \(location)")
print("Start location in image is \(boxLocation)")
animator.removeAllBehaviors()
let centerOffset = UIOffset(horizontal: boxLocation.x - self.bounds.midX, vertical: boxLocation.y - self.bounds.midY)
attachmentBehavior = UIAttachmentBehavior(item: self, offsetFromCenter: centerOffset, attachedToAnchor: location)
animator.addBehavior(attachmentBehavior)
case .ended:
print("Your touch end position is \(location)")
print("End location in image is \(boxLocation)")
animator.removeAllBehaviors()
// 1
let velocity = sender.velocity(in: emojiSuperView!)
let magnitude = sqrt((velocity.x * velocity.x) + (velocity.y * velocity.y))
if magnitude > ThrowingThreshold {
// 2
let pushBehavior = UIPushBehavior(items: [self], mode: .instantaneous)
pushBehavior.pushDirection = CGVector(dx: velocity.x / 10, dy: velocity.y / 10)
pushBehavior.magnitude = magnitude / ThrowingVelocityPadding
self.pushBehavior = pushBehavior
animator.addBehavior(pushBehavior)
// 3
let angle = Int(arc4random_uniform(20)) - 10
itemBehavior = UIDynamicItemBehavior(items: [self])
itemBehavior.friction = 0.2
itemBehavior.allowsRotation = true
itemBehavior.addAngularVelocity(CGFloat(angle), for: self)
animator.addBehavior(itemBehavior)
}
default:
attachmentBehavior.anchorPoint = sender.location(in: emojiSuperView!)
break
}
}
func recognizePinchGesture(sender: UIPinchGestureRecognizer)
{
weak var dynamicItem: UIDynamicItem?
// whatever your item is, probably a UIView
dynamicItem = self
let behavior = UIGravityBehavior(items: [dynamicItem!])
let animator = UIDynamicAnimator(referenceView: emojiSuperView!)
// or however you're getting your animator
animator.addBehavior(behavior)
sender.view!.transform = sender.view!.transform.scaledBy(x: sender.scale, y: sender.scale)
animator.updateItem(usingCurrentState: self)
self.animator.updateItem(usingCurrentState: self)
sender.scale = 1
}
When a user does any transform event, save current transform to a global variable.
after that when panning start assigns new transform in began state using UIAttachmentBehavior's action property.
attachmentBehavior.action = {
self.attachmentBehavior.items[0].transform = self.aTransform
}

UIGestureRecognizer - detach SKSpriteNode after dragging

I am using UIPanGestureRecognizer to rotate my sprite nodes with one finger (i.e. dragging the sprite up rotates it clockwise, and down anti clockwise).
This is my piece of code that works for the above function
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
self.view!.addGestureRecognizer(gestureRecognizer)
handlePan(gestureRecognizer)
func handlePan(_ gestureRecognizer:UIPanGestureRecognizer){
if gestureRecognizer.state == .began {
var touchLocation = gestureRecognizer.location(in: gestureRecognizer.view)
touchLocation = self.convertPoint(fromView: touchLocation)
gestureRecognizer.minimumNumberOfTouches = 1
self.selectTouchedNode(touchLocation: touchLocation)
}
if gestureRecognizer.state == .changed {
var touchLocation = gestureRecognizer.location(in: gestureRecognizer.view)
touchLocation = self.convertPoint(fromView: touchLocation)
var translation = gestureRecognizer.translation(in: gestureRecognizer.view!)
translation = CGPoint(x: translation.x, y: translation.y)
self.rotateNode(translation: translation)
}
if gestureRecognizer.state == .ended {
selectedNode.removeAllActions()
}
}
func selectTouchedNode(touchLocation: CGPoint) {
let touchedNode = self.atPoint(touchLocation)
if touchedNode is SKSpriteNode{
selectedNode = touchedNode as! SKSpriteNode
print ("touched")
} else {
selectedNode.removeAllActions()
}
}
func rotateNode(translation: CGPoint) {
selectedNode.zRotation = translation.y * 0.01
print ("Rotate")
print (translation.y)
}
However, my sprite nodes becomes "sticky" after I drag, i.e. if i've dragged sprite1, dragging the background of the scene still rotates sprite 1 until i drag sprite2, which then rotates sprite2 until I drag another node.
How can I "detach" the nodes so that it only rotates when I start the drag on their position?

Resources