Swift - Rotate gesture and rotation increments of 90 degrees
I achieved this by implementing it as follows:
@IBAction func handleRotation(_ recognizer: UIRotationGestureRecognizer) {
if let recognizerView = recognizer.view {
recognizerView.transform = recognizerView.transform.rotated(by: recognizer.rotation)
recognizer.rotation = 0
let radians:Double = atan2( Double(recognizerView.transform.b), Double(recognizerView.transform.a))
let degrees = radians * Double((180 / Float.pi))
if recognizer.state == .ended || recognizer.state == .cancelled {
var degreeToAnimate:CGFloat = 0
switch degrees {
case -45...45:
print("the default value 0, no need to any assign...")
case 46...135:
degreeToAnimate = CGFloat(M_PI_2)
case 136...180, -180 ... -136:
degreeToAnimate = CGFloat(M_PI)
case -135 ... -46:
degreeToAnimate = CGFloat(-M_PI_2)
default:
print("!")
}
UIView.animate(withDuration: 0.3, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 1.0, options: .curveEaseIn, animations: {
recognizerView.transform = CGAffineTransform(rotationAngle: degreeToAnimate)
}, completion: { _ in
recognizer.rotation = 0
})
}
}
}
Note that I added the UIRotationGestureRecognizer
on the desired view from the Interface Builder, that's why the function is an @IBAction
.
Output:
SceneKit - rotate SCNNode with pan gesture based on direction
After a LOT of research and few hours of head-banging the wall, i finally found a working solution.
My working code:
func handlePan(sender: UIPanGestureRecognizer){
// get pan direction
let velocity: CGPoint = sender.velocity(in: sender.view!)
if self.panDirection == nil {
self.panDirection = GameHelper.getPanDirection(velocity: velocity)
}
// if selected brick
if self.selectedBrickNode != nil {
if sender.state == UIGestureRecognizerState.began{
lastAngle = 0.0 // reset last angle
}
let translation = sender.translation(in: sender.view!)
let anglePan = (self.panDirection == "horizontal") ? GameHelper.deg2rad(deg: Float(translation.x)) : GameHelper.deg2rad(deg: Float(translation.y))
let x:Float = (self.panDirection == "vertical" ) ? 1 : 0.0
let y:Float = (self.panDirection == "horizontal" ) ? 1 : 0.0
// calculate the angle change from last call
let fraction = anglePan - lastAngle
lastAngle = anglePan
// perform rotation by difference to last angle
selectedBrickNode.transform = SCNMatrix4Mult(selectedBrickNode.transform,SCNMatrix4MakeRotation(fraction, x, y, 0.0))
if(sender.state == UIGestureRecognizerState.ended) {
// calculate angle to snap to 90 degree increments
let finalRotation = GameHelper.rad2deg(rad:anglePan)
let diff = finalRotation.truncatingRemainder(dividingBy: 90.0)
var finalDiff = Float(0.0)
switch diff {
case 45..<90 :
finalDiff = 90 - diff
case 0..<45 :
finalDiff = -diff
case -45..<0 :
finalDiff = abs(diff)
case -90 ..< -45 :
finalDiff = -90 - diff
default:
print("do nothing")
}
// final transform to apply snap to closest 90deg increment
let snapAngle = GameHelper.deg2rad(deg: finalDiff)
selectedBrickNode.transform = SCNMatrix4Mult(selectedBrickNode.transform, SCNMatrix4MakeRotation(snapAngle, x, y, 0.0))
// remove highlight from node and deselect
self.selectedBrickNode?.geometry?.materials = [hlp.defaultMaterial]
self.selectedBrickNode = nil
}
}
}
and GameHelper.swift
class GameHelper {
static func rad2deg( rad:Float ) -> Float {
return rad * (Float) (180.0 / Double.pi)
}
static func deg2rad( deg:Float ) -> Float{
return deg * (Float)(Double.pi / 180)
}
static func getPanDirection(velocity: CGPoint) -> String {
var panDirection:String = ""
if ( velocity.x > 0 && velocity.x > abs(velocity.y) || velocity.x < 0 && abs(velocity.x) > abs(velocity.y) ){
panDirection = "horizontal"
}
if ( velocity.y < 0 && abs(velocity.y) > abs(velocity.x) || velocity.y > 0 && velocity.y > abs(velocity.x)) {
panDirection = "vertical"
}
return panDirection
}
}
Scenekit - multiple rotation of SCNNode and direction of axes
After further research i realised i chose completely wrong approach. The way to achieve what i want was to rotate the node using the axes of the rootNode - this way i don't need to worry about local axes of my node.
EDIT: updated code based on Xartec's suggestions
let rotation = SCNMatrix4MakeRotation(angle, x, y, Float(0))
let newTransform = SCNMatrix4Mult(bricksNode.transform, rotation)
let animation = CABasicAnimation(keyPath: "transform")
animation.fromValue = bricksNode.transform
animation.toValue = scnScene.rootNode.convertTransform(newTransform,from: nil)
animation.duration = 0.5
bricksNode.addAnimation(animation, forKey: nil)
How to Rotate a UIImage 90 degrees?
What about something like:
static inline double radians (double degrees) {return degrees * M_PI/180;}
UIImage* rotate(UIImage* src, UIImageOrientation orientation)
{
UIGraphicsBeginImageContext(src.size);
CGContextRef context = UIGraphicsGetCurrentContext();
if (orientation == UIImageOrientationRight) {
CGContextRotateCTM (context, radians(90));
} else if (orientation == UIImageOrientationLeft) {
CGContextRotateCTM (context, radians(-90));
} else if (orientation == UIImageOrientationDown) {
// NOTHING
} else if (orientation == UIImageOrientationUp) {
CGContextRotateCTM (context, radians(90));
}
[src drawAtPoint:CGPointMake(0, 0)];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Code for a dedicated rotation button
There's a project on GitHub called KTOneFingerRotationGestureRecognizer that is a custom rotation gesture recognizer that uses a single finger to do rotations. You touch down on an object and spin it as if it had an axle in it's center. It might work for you, depending on how small your shapes are. You might need to make your views a little larger the the images they contain (add some padding, in other words.)
How to animate to rotate counter-clockwise
Just rotate it by whatever angle you did for clockwise, but multiply it by negative one
func rotateLeft() {
UIView.animate(withDuration: 0.5, animations: {
self.profileImageView.transform = CGAffineTransform(rotationAngle: ((180.0 * CGFloat(M_PI)) / 180.0) * -1)
self.profileImageView.transform = CGAffineTransform(rotationAngle: ((0.0 * CGFloat(M_PI)) / 360.0) * -1)
self.view.layoutIfNeeded()
})
}
I should also mention that when you say (180.0 * CGFloat(π)) / 180
- You're just saying π, because 180/180 is one.
It looks to me like you're trying to do two different rotations at once? I'm a little confused by that. One thing I've found that really helps is to track the view's axis orientation if you're doing multiple rotations both clockwise and counter-clockwise. Do something like create a property called viewAngle
var viewAngle: CGFloat = 0 // Right-side up to start
let π = CGFloat.pi // Swift allows special characters hold alt+p to use this, it allows for cleaner code
func rotate(by angle: CGFloat) {
for i in 0 ..< 4 {
UIView.animate(withDuration: 0.125, delay: 0.125 * Double(i), options: .curveLinear, animations: {
self.viewAngle += angle/4
self.rotateView.transform = CGAffineTransform(rotationAngle: self.viewAngle)
self.view.layoutIfNeeded()
}, completion: nil)
}
}
If you want to rotate right pass in a positive angle (i.e. π, π/2, π/4, 2*π), or if left pass in a negative angle.
Rotate right by 180 degrees:
rotate(by: π)
Rotate left by 180 degrees:
rotate(by: -π)
EDIT: As you were mentioning, which I didn't see until I tried for myself, when you plug in π or 2π whether it's negative or positive, it just rotates clockwise. To get around this I had to make it rotate by -π/4 increments. So to do a full-circle rotate counter clockwise I did a loop of -π/4 animations if that makes sense. It works, but I feel like there should be an easier way to do this. I would appreciate anyone else chiming in.
Bendable arm rotation in SpriteKit - rotate at point
You want to use an SKPhysicsJointPin
Here are the available Sprite-Kit joints :https://developer.apple.com/reference/spritekit/skphysicsjoint
You add or remove joints using the physics world. When you create a joint, the points that connect the joint are always specified in the scene’s coordinate system. This may require you to first convert from node coordinates to scene coordinates before creating a joint.
To use a physics joint in your game, follow these steps:
- Create two physics bodies.
- Attach the physics bodies to a pair of SKNode objects in the scene.
- Create a joint object using one of the subclasses listed in the
diagram above. If necessary, configure the joint object’s properties
to define how the joint should operate. - Retrieve the scene’s SKPhysicsWorld object.
- Call the physics world’s add(_:) method.
The following code pins two nodes, upperArm
and lowerArm
, together using a 'elbow' implemented via an , SKPhysicsJointPin
joint. We calculate the 'elbow' position by taking the middle of the area where upperArm
and lowerArm
overlap - this is pretty crude and should be improved.
let elbowArea = lowerArm.frame.intersection(upperArm.frame)
let elbowPosition = CGPoint(x: elbowArea.midX, y: elbowArea.midY)
let elbowJoint = SKPhysicsJointPin.joint(withBodyA: upperArm.physicsBody!,
bodyB: lowerArm.physicsBody!,
anchor: elbowPosition)
scene.physicsWorld.add(elbowJoint)
You should also set rotation limits on the pin joint (elbow) via the joint's properties:
var shouldEnableLimits: Bool
A Boolean value that indicates whether
the pin joint’s rotation is limited to a specific range of values.
var lowerAngleLimit: CGFloat
The smallest angle allowed for the pin
joint, in radians.
var upperAngleLimit: CGFloat
The largest angle allowed for the pin
joint, in radians.
Rotate an UIView with CGAffineTransformMakeRotation
When you add the transform to myView you are not applying a transform to it's existing transformed state, but rather overwriting the transform. This means that when you set transform = CGAffineTransformMakeRotation(M_PI/2) for the second time, you are not actually changing the transform at all.
EDIT:
As Rob pointed out, there is no need to use the property rotationInRadians when the rotation value is already stored in the transform. Instead you can simply modify the existing transform like this:
[UIView animateWithDuration:0.2 animations:^{
_myView.transform = CGAffineTransformRotate(_myView.transform, M_PI/2);
}];
Below is the not ideal solution that I previously presented.
You will need a variable that keeps track of your rotation. You will increment this variable by your desired amount each time you want to rotate, then create a new transform based on the new value of the variable.
Make a property to do this:
@property (nonatomic, assign) CGFloat rotationInRadians;
Initialize it to 0 in your init method. Then modify your rotateView method to increment rotationInRadians and apply the new transform.
- (IBAction)rotateView:(id)sender {
self.rotationInRadians += M_PI/2;
[UIView animateWithDuration:0.2 animations:^{
_myView.transform = CGAffineTransformMakeRotation(self.rotationInRadians);
}];
}
Converting Radians to Degrees using UIRotationGestureRecognizer?
Your main issue is that you are resetting the gesture's rotation
property. You should not do that. From the documentation:
The rotation value is a single value that varies over time. It is not the delta value from the last time that the rotation was reported. Apply the rotation value to the state of the view when the gesture is first recognized—do not concatenate the value each time the handler is called.
So remove the sender.rotation = 0
line.
As a result of that, you need to fix how you apply the transform to the image view.
Replace:
imageView.transform = imageView.transform.rotated(by: sender.rotation)
with:
imageView.transform = CGAffineTransform(rotatationAngle: sender.rotation)
That applies the full rotation instead of trying to increment the current rotation.
Related Topics
How to Insert UIlabel After UItextfield in UIalertcontroller
Add Assets to an Icloud Shared Photo Album Programmatically
Completion Handler in For/In Loop - Swift
Swift Ensembles Set Up & Ubiquitycontaineridentifier
Swift3 Different Font in The All of The UIview with Localization Each
Type Alias Declaration with Templates in Swift
Why My Arguments Are Being Blocked When Running a Shell Command
Swift: Gradient Splits on Rotation
How to Get The Range of The First Line in a String
Swift: Handling an Unexpected Nil Value, When Variable Is Not Optional
Creating Semaphore with Initial Value of 0 Make Issues with Execution
How to Post Parameter with (+ Plus Sign) in Alamofire
Implement a Custom Staggeregrid in UIview Like Etsy App in Swift
How to Calculate a Distance Between Two Anchorentities
Cloudkit - What to Do When a User Adds, Modifies or Deletes an Object While Offline
.Childadded Observer Doesn't Get Called If The Function Is Not Called Explicitly. Swift 4