Swift Continuous Rotation Animation Not So Continuous

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:

  1. 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()
    }
    }
  2. 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 view transform where it was. Then, to resume the rotation, just add a UIDynamicItemBehavior 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).

  3. 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 the presentationLayer 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
    }
  4. 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 grab m12 and m11 of the CATransform3D:

     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
    }
  5. 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
}
}

Demo of padlock with animated circle

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|UIViewKeyframeAnimationOpti‌​ons.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



Leave a reply



Submit