Swift Continuous Rotation Animation not so continuous
I'm not sure what's wrong with your code, but I've implemented continuous rotation using this method,
@IBAction func rotateView(sender: UIButton) {
UIView.animate(withDuration: 0.5, delay: 0, options: .curveLinear, animations: { () -> Void in
self.spinningView.transform = self.spinningView.transform.rotated(by: .pi / 2)
}) { (finished) -> Void in
self.rotateView(sender: sender)
}
}
Proper way to stop an infinitely rotating image? and how does one implement removeAllAnimations?
There are a couple of ways to do what you're asking about:
If supporting iOS 10+, you can use
UIViewPropertyAnimator
, whose animations you can pause and restart (resuming from where it was paused):private var animator: UIViewPropertyAnimator?
deinit {
animator?.stopAnimation(true)
}
@IBAction func didTapButton(_ sender: Any) {
guard let animator = animator else {
createAnimation()
return
}
if animator.isRunning {
animator.pauseAnimation()
} else {
animator.startAnimation()
}
}
/// Create and start 360 degree animation
///
/// This will fire off another animation when one 360° rotation finishes.
private func createAnimation() {
animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 4, delay: 0, options: .curveLinear) { [self] in
UIView.animateKeyframes(withDuration: 4, delay: 0) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 / 3.0) {
animatedView.transform = .init(rotationAngle: .pi * 2 * 1 / 3)
}
UIView.addKeyframe(withRelativeStartTime: 1.0 / 3.0, relativeDuration: 1.0 / 3.0) {
animatedView.transform = .init(rotationAngle: .pi * 2 * 2 / 3)
}
UIView.addKeyframe(withRelativeStartTime: 2.0 / 3.0, relativeDuration: 1.0 / 3.0) {
animatedView.transform = .identity
}
}
} completion: { [weak self] _ in
self?.createAnimation()
}
}You can alternatively use UIKit Dynamics to rotate the item. You can then remove a
UIDynamicItemBehavior
that was performing the rotation and it just stops where it was. It automatically leaves the viewtransform
where it was. Then, to resume the rotation, just add aUIDynamicItemBehavior
for the rotation again:private lazy var animator = UIDynamicAnimator(referenceView: view)
private var rotate: UIDynamicItemBehavior!
@IBAction func didTapButton(_ sender: Any) {
if let rotate = rotate {
animator.removeBehavior(rotate)
self.rotate = nil
} else {
rotate = UIDynamicItemBehavior(items: [animatedView])
rotate.allowsRotation = true
rotate.angularResistance = 0
rotate.addAngularVelocity(1, for: animatedView)
animator.addBehavior(rotate)
}
}This doesn't let you easily control the speed of the rotation in terms of time, but rather it’s dictated by
angularVelocity
, but it's a nice simple approach (and supports iOS 7.0 and later).The old-school approach for stopping an animation and leaving it where you stopped it is to capture the
presentationLayer
of the animation (which shows where it was mid-flight). Then you can grab the current state, stop the animation, and set the transform to what thepresentationLayer
reported.private var isAnimating = false
@IBAction func didTapButton(_ sender: Any) {
if isAnimating {
let transform = animatedView.layer.presentation()!.transform
animatedView.layer.removeAllAnimations()
animatedView.layer.transform = transform
} else {
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.byValue = 2 * CGFloat.pi
rotate.duration = 4
rotate.repeatCount = .greatestFiniteMagnitude
animatedView.layer.add(rotate, forKey: nil)
}
isAnimating = !isAnimating
}If you want to use
UIView
block based animation, you have to capture the angle at which you stopped the animation, so you know from where to restart the animation. The trick is grabm12
andm11
of theCATransform3D
:angle = atan2(transform.m12, transform.m11)
Thus, this yields:
private var angle: CGFloat = 0
private var isAnimating = false
@IBAction func didTapButton(_ sender: Any) {
if isAnimating {
let transform = animatedView.layer.presentation()!.transform
angle = atan2(transform.m12, transform.m11)
animatedView.layer.removeAllAnimations()
animatedView.layer.transform = transform
} else {
UIView.animate(withDuration: 4, delay: 0, options: .curveLinear) { [self] in
UIView.animateKeyframes(withDuration: 4, delay: 0, options: .repeat) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 / 3.0) {
animatedView.transform = .init(rotationAngle: self.angle + .pi * 2 * 1 / 3)
}
UIView.addKeyframe(withRelativeStartTime: 1.0 / 3.0, relativeDuration: 1.0 / 3.0) {
animatedView.transform = .init(rotationAngle: self.angle + .pi * 2 * 2 / 3)
}
UIView.addKeyframe(withRelativeStartTime: 2.0 / 3.0, relativeDuration: 1.0 / 3.0) {
animatedView.transform = .init(rotationAngle: self.angle)
}
}
}
}
isAnimating = !isAnimating
}You can rotate the object yourself using
CADisplayLink
that updates the angle to some calculated value. Then stopping the rotation is as simple as invalidating the display link, thereby leaving it where it was when it stopped. You can then resume animation by simply adding the display link back to your runloop.This sort of technique gives you a great deal of control, but is the least elegant of the approaches.
Continuous Rotation of NSImageView (so it appears to be animated)
You could add an extension of UIView or UIImageView like this:
extension UIView {
///The less is the timeToRotate, the more fast the animation is !
func spinClockwise(timeToRotate: Double) {
startRotate(CGFloat(M_PI_2), timeToRotate: timeToRotate)
}
///The less is the timeToRotate, the more fast the animation is !
func spinAntiClockwise(timeToRotate: Double) {
startRotate(CGFloat(-M_PI_2), timeToRotate: timeToRotate)
}
func startRotate(angle: CGFloat, timeToRotate: Double) {
UIView.animateWithDuration(timeToRotate, delay: 0.0, options:[UIViewAnimationOptions.CurveLinear, UIViewAnimationOptions.Repeat], animations: {
self.transform = CGAffineTransformMakeRotation(angle)
}, completion: nil)
print("Start rotating")
}
func stopAnimations() {
self.layer.removeAllAnimations()
print("Stop rotating")
}
}
So when you want to rotate your myImg
, you just have to call:
myImg.spinClockwise(3)
And when you want to stop it:
myImg.stopAnimations()
NOTE:
I added a playground just so you can test it out ;)
Cheers!
EDIT:
My bad, Here is the example for NSView:
extension NSView {
///The less is the timeToRotate, the more fast the animation is !
func spinClockwise(timeToRotate: Double) {
startRotate(CGFloat(-1 * M_PI * 2.0), timeToRotate: timeToRotate)
}
///The less is the timeToRotate, the more fast the animation is !
func spinAntiClockwise(timeToRotate: Double) {
startRotate(CGFloat(M_PI * 2.0), timeToRotate: timeToRotate)
}
func startRotate(angle: CGFloat, timeToRotate: Double) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = angle
rotateAnimation.duration = timeToRotate
rotateAnimation.repeatCount = .infinity
self.layer?.addAnimation(rotateAnimation, forKey: nil)
Swift.print("Start rotating")
}
func stopAnimations() {
self.layer?.removeAllAnimations()
Swift.print("Stop rotating")
}
}
Important note: Now, after my tests, I noticed that you must set the anchor point of your NSView in the middle so that it can rotate around its center:
view.layer?.anchorPoint = CGPointMake(0.5, 0.5)
I added a new playground with the OSX example
Animate drawing of full circle continuously without any lag during restart using CABasicAnimation
Rather than trying to animate the actual drawing, just draw the view once and then animate it.
Here is a custom PadlockView
and a custom CircleView
which mimic the animation you showed. To use it, add the code below to your project. Add a UIView
to your Storyboard, change its class to PadlockView
, and make an @IBOutlet
to it (called padlock
perhaps). When you want the view to animate, set padlock.circle.isAnimating = true
. To stop animating, set padlock.circle.isAnimating = false
.
CircleView.swift
// This UIView extension was borrowed from @keval's answer:
// https://stackoverflow.com/a/41160100/1630618
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 3) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat.pi * 2
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
}
class CircleView: UIView {
var foregroundColor = UIColor.white
var lineWidth: CGFloat = 3.0
var isAnimating = false {
didSet {
if isAnimating {
self.isHidden = false
self.rotate360Degrees(duration: 1.0)
} else {
self.isHidden = true
self.layer.removeAllAnimations()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.isHidden = true
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
let width = bounds.width
let height = bounds.height
let radius = (min(width, height) - lineWidth) / 2.0
var currentPoint = CGPoint(x: width / 2.0 + radius, y: height / 2.0)
var priorAngle = CGFloat(360)
for angle in stride(from: CGFloat(360), through: 0, by: -2) {
let path = UIBezierPath()
path.lineWidth = lineWidth
path.move(to: currentPoint)
currentPoint = CGPoint(x: width / 2.0 + cos(angle * .pi / 180.0) * radius, y: height / 2.0 + sin(angle * .pi / 180.0) * radius)
path.addArc(withCenter: CGPoint(x: width / 2.0, y: height / 2.0), radius: radius, startAngle: priorAngle * .pi / 180.0 , endAngle: angle * .pi / 180.0, clockwise: false)
priorAngle = angle
foregroundColor.withAlphaComponent(angle/360.0).setStroke()
path.stroke()
}
}
}
PadlockView.swift
class PadlockView: UIView {
var circle: CircleView!
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
func setup() {
self.backgroundColor = .clear
circle = CircleView()
circle.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(circle)
circle.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
circle.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
circle.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
circle.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
}
override func draw(_ rect: CGRect) {
let width = bounds.width
let height = bounds.height
let lockwidth = width / 3
let lockheight = height / 4
let boltwidth = lockwidth * 2 / 3
UIColor.white.setStroke()
let path = UIBezierPath()
path.move(to: CGPoint(x: (width - lockwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width + lockwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width + lockwidth) / 2, y: height / 2 + lockheight))
path.addLine(to: CGPoint(x: (width - lockwidth) / 2, y: height / 2 + lockheight))
path.close()
path.move(to: CGPoint(x: (width - boltwidth) / 2, y: height / 2))
path.addLine(to: CGPoint(x: (width - boltwidth) / 2, y: height / 2 - boltwidth / 4))
path.addArc(withCenter: CGPoint(x: width/2, y: height / 2 - boltwidth / 4), radius: boltwidth / 2, startAngle: .pi, endAngle: 0, clockwise: true)
path.lineWidth = 2.0
path.stroke()
}
}
Note: Continuous animation code courtesy of this answer.
Here is a demo that I setup with the following code in my ViewController
:
@IBOutlet weak var padlock: PadlockView!
@IBAction func startStop(_ sender: UIButton) {
if sender.currentTitle == "Start" {
sender.setTitle("Stop", for: .normal)
padlock.circle.isAnimating = true
} else {
sender.setTitle("Start", for: .normal)
padlock.circle.isAnimating = false
}
}
UIView Infinite 360 degree rotation animation?
Found a method (I modified it a bit) that worked perfectly for me: iphone UIImageView rotation
#import <QuartzCore/QuartzCore.h>
- (void) runSpinAnimationOnView:(UIView*)view duration:(CGFloat)duration rotations:(CGFloat)rotations repeat:(float)repeat {
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: M_PI * 2.0 /* full rotation*/ * rotations * duration ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.repeatCount = repeat ? HUGE_VALF : 0;
[view.layer addAnimation:rotationAnimation forKey:@"rotationAnimation"];
}
Create a continuously rotating square on the screen using animateKeyframesWithDuration
That's really odd... UIView.animateKeyframesWithDuration
isn't working as I would expect it to with UIViewKeyframeAnimationOptions.CalculationModeLinear|UIViewKeyframeAnimationOptions.Repeat
passed in with options.
If you use the non-block method of creating a keyframe animation (see below) the rotation repeats as expected.
If I find out why the block-based option isn't working I'll try and remember to update answer here too!
override func viewDidLoad() {
super.viewDidLoad()
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
let fullRotation = CGFloat(M_PI * 2)
let animation = CAKeyframeAnimation()
animation.keyPath = "transform.rotation.z"
animation.duration = 2
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
animation.repeatCount = Float.infinity
animation.values = [fullRotation/4, fullRotation/2, fullRotation*3/4, fullRotation]
square.layer.addAnimation(animation, forKey: "rotate")
}
I need to infinitely rotate a view until told to stop
Actually it would be more easy:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 1.0) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat.pi * 2
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
func stopRotating(){
self.layer.sublayers?.removeAll()
//or
self.layer.removeAllAnimations()
}
}
Then for rotating:
yourView.rotate360Degrees()
for stopping:
yourView. stopRotating()
how to spin an image continuously
You are better of doing this with a CABasicAnimation
:
if ([self.spinnerOverlay animationForKey:@"SpinAnimation"] == nil) {
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat: 2*M_PI];
animation.duration = 10.0f;
animation.repeatCount = INFINITY;
[self.spinnerOverlay.layer addAnimation:animation forKey:@"SpinAnimation"];
}
In this code I check whether the animation is all ready set, not need to set it again.
The spinnerOverlay
is in your case the UIImageView
you want to rotate.
To stop the animation:
[self.spinnerOverlay.layer removeAnimationForKey:@"SpinAnimation"];
Related Topics
Passing Data Between View Controllers Without Segue
Get All Urls for Resources in Sub-Directory in Swift
Swift Get Specific Value from Firebase Database
How to Eliminate the Margin on the Left Side of a Uitableview, Without Creating a Gap on the Right
How to Get All Keys and Values into Separate String Arrays from Nsdictionary in Swift
How to Convert Date (Without Knowing the Kind) to String
iOS - Uinavigationcontroller, Hide Navigationbar
Avplayer Not Full Screen in Landscape Mode Using iOS Swift
iOS Notification Content Extension: Add Buttons in Storyboard and Handle the Click Action
Swiftui: How to Change the Tint Color (Background Color) of a Navigationview
Using Haneke to Cache Then Play Mp4 Files with Avplayer
Errortype' Is Not Convertible to 'Nserror'
iOS with Parse. Pfuser.Currentuser() Not Getting Cached. Returns Nil After App Restart
Swift - Uipopovercontroller in iOS 8
Spritekit/Swift - How to Check Contact of Two Nodes When They Are Already in Contact