Rotate Animation in Swift

Updated on December 14, 2015 – Swift 2.0 + new examples

With this post, I intend to wrap up my series on animations as UIView Extensions in Swift… for now.  Truthfully, these ideas flowed out of a real-world app that I was working on, which required various simple animations (fading in/out, sliding text, and now, rotating a view 360 degrees).

Since I’ve given two other detailed walk-throughs on the topic, I’ll try to be to-the-point on this one.

As with the others, I’ve created a GitHub project for you to see the animation in action, and even modify to your liking.

The Extension

The following code adds a method to any UIView instance called rotate360Degrees. The code can be placed in a Swift file called “UIViewExtensions.swift”:

 1import UIKit
 2
 3extension UIView {
 4    func rotate360Degrees(duration: CFTimeInterval = 1.0, completionDelegate: AnyObject? = nil) {
 5        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
 6        rotateAnimation.fromValue = 0.0
 7        rotateAnimation.toValue = CGFloat(M_PI * 2.0)
 8        rotateAnimation.duration = duration
 9        
10        if let delegate: AnyObject = completionDelegate {
11            rotateAnimation.delegate = delegate
12        }
13        self.layer.addAnimation(rotateAnimation, forKey: nil)
14    }
15}

The only critical thing to notice in the above code snippet is the value passed to the CABasicAnimation constructor.  The "transform.rotation” string is what sets things up to go spinning, and the string must be typed exactly as-is for the animation to work.

As in my previous animation posts, I provide myself a couple of parameters to set for a little bit of customization if I want it. Since the parameters have default values, the method can be invoked by writing someUIViewInstance.rotate360Degrees() for simple cases.  For more “advanced” scenarios where you need to adjust how long the animation takes, or to perform some logic after the animation completes, you can pass in a duration value other than 1.0, assign a completionDelegate, or both, depending on your needs.

Check out the GitHub example for details on how to configure things for the completionDelegate.  I’ll be walking through that more “advanced” case shortly as well.

Example

Perhaps you’re asking, “Why spinning UIViews?”…

In my example, I’ve proposed a simple button that would be used to refresh the view / data in a real-world scenario.  When the button is tapped, I want the button to rotate 360 degrees.

In the “advanced” example, I want it to rotate continually until a process of some sort finishes, at which point the animation stops until initiated again.  Take a look:

Rotate Animation Example

 

Simple Case – Rotate Once

Once the UIView extension is in place, the simple use case is… well… pretty simple:

1class ViewController: UIViewController {
2    @IBOutlet weak var refreshButton: UIButton!
3    
4    @IBAction func refresh() {
5        self.refreshButton.rotate360Degrees()
6        // Perhaps start a process which will refresh the UI...
7    }
8}

“Advanced” Case – Rotate Until Process Finishes

In my example, I decided to simulate a long-running process by using a custom-built Timer class, heavily inspired by Samuel Mullen’s implementation (with a few modifications to fit my needs).  If you’re looking through the GitHub example, try not to get too bogged down in the details of the Timer, unless it just intrigues you.  In real life, you may decide perform a web service call to refresh your data model, or refresh your UI (or both).  Whatever the case may be, you’ll likely end up with similar logic:

  • Refresh button is tapped
  • If the button isn’t already rotating, make it start
  • Kick off a process that may take some time
  • The animationDidStop callback is going to be invoked after the view has spun a full 360 degrees.  If the longish-running process is finished, the button can stop spinning.  Otherwise, it needs to spin around another time.  This will be repeated until the longish-running process is complete.

Confession:  I’m not entirely thrilled with the rampant mutability in my implementation, but I couldn’t figure out a way to do what I wanted in an immutable way.  It does work, however.  Just be aware that if you’re really a stickler for immutability in your classes, you’re going to hate this implementation (and I’d love to hear constructive feedback on how I could improve it!).  Here’s the code for the bullet-pointed process just outlined:

 1class ViewController2: UIViewController {
 2    @IBOutlet weak var refreshButton: UIButton!
 3    // var, var, var!  So much for immutability :/
 4    var isRotating = false
 5    var shouldStopRotating = false
 6    var timer: Timer!
 7    
 8    @IBAction func refresh() {
 9        if self.isRotating == false {
10            self.refreshButton.rotate360Degrees(completionDelegate: self)
11            // Perhaps start a process which will refresh the UI...
12            self.timer = Timer(duration: 5.0, completionHandler: {
13                self.shouldStopRotating = true
14            })
15            self.timer.start()
16            self.isRotating = true
17        }
18    }
19    
20    override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
21        if self.shouldStopRotating == false {
22            self.refreshButton.rotate360Degrees(completionDelegate: self)
23        } else {
24            self.reset()
25        }
26    }
27    
28    func reset() {
29        self.isRotating = false
30        self.shouldStopRotating = false
31    }
32}

Summary

I tried to strike a balance between making these simple animations easy to call on my labels, buttons, and other UIView subclasses, and just shoving everything into a UIViewExtensions.swift file.  The related set of animations just seemed like a really nice use case for Swift extensions, and the strategy served me well in a recent project.  Hopefully the series has sparked some ideas in your mind for how to employ extensions to enhance the capabilities of a class so that your code is easier and cleaner to write.

As always – thanks for reading!

comments powered by Disqus