Welcome to Creitive Global

Let's start

Partner with us

biz@creitive.com

Work with us

office@creitive.com

Animations: The more advanced way.

The first thing you see when unlocking your iPhone is an animation. The clock and part of the UI components fly back into the abyss, while the app icons appear on the screen. Then you swipe to find your latest message or email, and while doing so, you can see a swift transition between screens. Opening the app, loading the content, showing the keyboard, a button click here, a button click there, a drop menu from the left, from the right, everything flies on the screen, everything is animated so elegantly, giving you an impression that the mobile is alive. You can see every step your actions take, it's that easy.

But when you start to code, after a while you manage to create an animated drop down, then you try to add a fade effect, and already you can see that the animation doesn't go so smoothly, the fading is faster (it's okay, you'll fix it later), then you add a swipe and everything crumbles. Why? How?

Let me try to explain the world of animations using a small test project which we'll call the planetary model.

Core Animation is a huge framework, and as much as it sounds silly, it's not all about animations. There are transitions, layers, many tasks only related to presenting graphics on the screen etc. I will just write about a really small part of this world, explicit animations - animations you create where needed. You may wonder why I said "explicit" - of course that you write the code - but there are animations that Swift performs automatically (implicit animations).

Implicit animations

Everytime you change some property of CALayer (we can say that UIView is just simple wrapper around CALayer), it doesn't happen instantly; instead, Core Animation smoothly animates a transition between the two values, and this happens automatically. Such animations are called implicit because you don't specify the way the animation happens - everything is taken care for you. If you ever wondered, the default animation time speed is 0.25 seconds. I won't bother you much about layer trees and transactions, I will let you explore, but I must mention that everything on the screen is there waiting to be animated, and while doing so it can be represented with different layers. The actual view may not actually be on the screen where it is currently drawn, but you'll see its presentation layer, thus data values from the model layer differ. Now back to the topic.

Explicit animations

Often you are faced with problems where implicit animations can't help you. You need a more powerful tool to express your idea. Now is the perfect time to play with explicit animations. Core Animation offers a few different ways to solve those problems. I will cover animations from the aspect of how classes are organized in the framework. We will take a look at property animations, animation groups and transitions.

Property animations

Property animations target a single property of a layer, changing its value or a range of values. Such property animations are further split into basic and key frame animations. You may have already worked with basic animation unknowingly. UIView.Animation is more or less the same as CABasicAnimation. It's a simple wrapper you can use with a completion block and arguments to change properties on the view. Basic animation allows us to change a property using tree attributes, fromValue, toValue and byValue. CAKeyFrameAnimation is more powerful than basic animation, it has no interface exposed in UIKit. It's not limited to a single start and end value, instead it can be given an arbitrary sequence of values to animate between.

Transition animations

Sometimes we are faced with problems bigger than changing the values of a property or two. In such cases, Swift offers us transitions. A transition is used for animating non-animatable content, for example when you need to remove or add a few views on the screen, you can't just change those view properties.

Animation groups

Multiple animations such as CABasicAnimation and CAKeyFrameAnimation can be grouped together using CAAnimationGroup. It's also useful for setting the same animation duration or adding and removing a collection of animations from a layer.

Example

Now that we've covered some basics, let's write some code.

I skipped creating a new project in Xcode and moved directly to our planetary model. The idea is to create an animated view where objects (planets) will orbit around some position in space (let's place the Sun in the center of the view).

We don't care about the rotating objects, it could be some UIControl, UIImageView or whatever you want.

The idea is just to have controls rotating around some central objects on provided orbits. To make things more interesting, the center of the planetary model - the Sun - can rotate slowly, as well as radiate.

Let's create a class and inherit UIView.

// Planetary model view class will represent whole animated
// view with all orbits and objects
@IBDesignable
class PlanetaryModelView : UIView {
}

Nice, let's create orbits.

@IBDesignable
class PlanetaryModelView : UIView {
    // Number of orbits
    @IBInspectable var numberOfOrbits: Int = 1 {
        didSet { drawOrbits() }
    }

    // How much is first orbit distant from the view center
    @IBInspectable var centerOffset: CGFloat = 0.00 {
        didSet { drawOrbits() }
    }

    // How much is the last orbit distant from the border frame
    @IBInspectable var borderOffset: CGFloat = 0.00 {
        didSet { drawOrbits() }
    }

    // Orbit line width
    @IBInspectable var lineWidth: CGFloat = 0.50

    // Orbit color
    @IBInspectable var color: UIColor = UIColor.black

    // Array of orbits
    // Orbit is represented with UIBezierPath, because we could
    // easily create wrapper if needed or use CAShapeLayer to
    // customize every orbit in a specific way
    var orbits = [UIBezierPath]()
}

At this point we are missing a function for creating and drawing orbits (based on UIBezierPath).

@IBDesignable
class PlanetaryModelView : UIView {
    //... rest of code ...

    // Method will create UIBezierPaths and store them in a
    // array (self.orbits) every time someone change system properties
    func drawOrbits() {
        let center = CGPoint(x: self.bounds.origin.x + self.bounds.size.width/2, y: self.bounds.origin.y + self.bounds.size.height/2)
        let radius = min(self.bounds.size.width, self.bounds.size.height)/CGFloat(self.numberOfOrbits)/2 - self.borderOffset

        orbits.removeAll()

        for index in 0.. < numberoforbits {
            let path = UIBezierPath(arcCenter:center, radius: radius*CGFloat(index) + centerOffset, startAngle: 0, endAngle: CGFloat(M_PI*2), clockwise: false)
            orbits.append(path)
            self.color.set()
            path.lineWidth = self.lineWidth
            path.stroke()
        }
    }
}

This code will draw orbits on the view in your storyboard/xib.

Now we need to create models for the planets and the Sun.

// Planet model, used to store data about object that will be placed on the orbit
struct Planet {
    var image: UIImage?
    var speed: CGFloat = 0.00
    var offset: CGFloat = 0.00
    var size: CGSize = CGSize.zero
    var orbit: Int = 0
    var rotateClockwise = true
}

// Sun model, used to store data about object that will be
// placed in the middle of the planetary system
struct Sun {
    var image: UIImage?
    var speed: CGFloat = 0.00
    var size: CGSize = CGSize.zero
    var rotateClockwise = true
}

Nice, we are almost there. Let's create an API for adding planets and the Sun to the view.

@IBDesignable
class PlanetaryModelView : UIView {
    // ... rest of code ...

    // Array of planets
    fileprivate var planets = [Planet]()

    // Method for adding planets to array
    func add(planet: Planet) {
            self.planets.append(planet)
    }

    // Sun
    fileprivate var sun: Sun?

    // Method for setting the sun object
    func add(sun: Sun) {
        self.sun = sun
    }

    // Array of containers view, every planet/sun will be wrapped
    // with UIImageView and as such we need to store all imageViews
    // in a array, so we can work with animation easily
    var containerViews = [UIImageView]()

    // We are limiting animation duration (speed) to range from 1 sec to 100 sec
    struct Globals {
        static let maxSpeed: CGFloat = 100.00
        static let minSpeed: CGFloat = 1.00
    }
}

Now to add functions for handling animation start/stop.

@IBDesignable
class PlanetaryModelView : UIView {
    // ... rest of code ...
    // Method for starting/pausing animation
    func animation(play startAnim: Bool) {
        self.stopAnim()
        if startAnim {
            self.startAnim()
        }
    }
    // Bool representing animation state
    var isViewAnimating: Bool = false
    // Method for starting animation
    fileprivate func startAnim() {
        self.isViewAnimating = true
        self.redrawPlanets()
    }
    // Method for stoping animation
    fileprivate func stopAnim() {
        self.isViewAnimating = false
        for containerView in self.containerViews {
            containerView.layer.removeAllAnimations()
        }
    }
}

Finally, the animation.

@IBDesignable
class PlanetaryModelView : UIView {
    // ... rest of code ...

    // Method that will create container views that will display animation
    func redrawPlanets() {
        // Remove old container views
        for containerView in self.containerViews {
            containerView.removeFromSuperview()
        }

        // Clear the array
        self.containerViews.removeAll()

        // Create new container view wrapper for every planet and
        // apply specific animation to it.
        for planet in self.planets {
            // Create container view wrapper
            let origin = CGPoint(x: -planet.size.width*0.50, y: -planet.size.height*0.50)
            let containerView = UIImageView(frame: CGRect(origin: origin, size: planet.size))
            containerView.image = planet.image
            self.containerViews.append(containerView)
            self.addSubview(containerView)

            // Create animation

            // Create CAKeyFrameAnimation with path 'position' because
            // we want to change planet position related to specific orbit
            let animation = CAKeyframeAnimation(keyPath: "position")

            // Set animation path
            animation.path = self.orbits[planet.orbit].cgPath

            // Set animation repeat count to infinity (huge number)
            animation.repeatCount = Float.infinity

            // Set calculation mode to Paced so animation ignore 'keyTimes'
            // and 'timingFunctions'
            animation.calculationMode = kCAAnimationPaced

            // Set rotation mode to Auto, so container view rotate together
            // with path curvature
            animation.rotationMode = kCAAnimationRotateAuto

            // Set is removed on completion to false so animation don't
            // reset presenting layer to model layer origin
            animation.isRemovedOnCompletion = false

            // This will keep the presenting layer on the place where
            // it's finished animation
            animation.fillMode = kCAFillModeForwards

            // Setting the speed to 1 or -1 we can set animation going
            // forward or backwards in time
            // If you set animation speed to 2, animation will complete
            // twice as fast
            animation.speed = planet.rotateClockwise ? -1.0 : 1.0

            // Set time offset so planet don't start animating from the
            // beginning of the path
            animation.timeOffset = TimeInterval(planet.offset)

            // Set duration (animation speed in seconds) so we can have
            // planets move in different speeds
            // If you set animation duration to 10 and speed to 2 animation
            // will complete in 5 seconds
            animation.duration = TimeInterval(max(min(Globals.maxSpeed - planet.speed, Globals.maxSpeed), Globals.minSpeed) )

            // Play animation
            containerView.layer.add(animation, forKey: nil)
        }

        // If sun exists we need to create new container view wrapper,
        // and apply specific animation to it
        if sun != nil {
            // Create container view wrapper
            let center = CGPoint(x: self.bounds.origin.x + self.bounds.size.width/2, y: self.bounds.origin.y + self.bounds.size.height/2)
            let origin = CGPoint(x: center.x - sun!.size.width/2, y: center.y - sun!.size.height/2)
            let containerView = UIImageView(frame: CGRect(origin: origin, size: sun!.size))
            containerView.image = sun!.image
            self.containerViews.append(containerView)
            self.addSubview(containerView)

            // - Create animation 1

            // Create CABasicAnimation with path 'transform.rotation.z'
            // because we want to slowly rotate sun
            let rotation = CABasicAnimation(keyPath: "transform.rotation.z")

            // Rotation will go from 0
            rotation.fromValue = 0

            // to 360 degrees, full circle
            rotation.toValue = CGFloat(M_PI*2)

            // Animation actual speed equal to it's duration
            rotation.speed = 1

            // - Create animation 2

            // Create CABasicAnimation with path 'transform.scale' because we
            // want create pulsing effect
            let pulsing = CABasicAnimation(keyPath: "transform.scale")

            // Transforming scale (size) of the container view from 1
            pulsing.fromValue = NSValue.init(caTransform3D: CATransform3DMakeScale(1.00, 1.00, 1.00))

            // to 1.2 (z coordinate can be ignored in 2D)
            pulsing.toValue = NSValue.init(caTransform3D: CATransform3DMakeScale(1.20, 1.20, 1.00))

            // Set autoreverses to true so animation play in backwards once it's finished
            pulsing.autoreverses = true

            // Set animation speed to 2, so animation can scale up and down in
            // same speed as rotation by 360 degrees
            pulsing.speed = 2

            // - Create animation 3

            // Create CABasicAnimation with path 'opacity' because we want
            // to create fade(glow) effect
            let fade = CABasicAnimation(keyPath: "opacity")

            // Changing alpha layer of the sun from 1
            fade.fromValue = 1.0

            // to 0.7 for example
            fade.toValue = 0.7

            // and with autoreverse
            fade.autoreverses = true

            // speed again at 2, we can create fade in/out effect
            fade.speed = 2

            // Create animation group CAAnimationGroup
            let animationGroup = CAAnimationGroup()

            // Add animations to animations array
            animationGroup.animations = [rotation, pulsing, fade]

            // Set animation group duration (all animations will be effected)
            animationGroup.duration = TimeInterval( max(min(Globals.maxSpeed - sun!.speed, Globals.maxSpeed), Globals.minSpeed) )

            // Same for other properties
            animationGroup.repeatCount = Float.infinity
            animationGroup.isRemovedOnCompletion = false
            animationGroup.fillMode = kCAFillModeForwards

            // Play animation for every animation in animation group
            containerView.layer.add(animationGroup, forKey: nil)
        }
    }
}

All you need to do now is to set your view controller.

class ViewController: UIViewController {
    @IBOutlet weak var planetaryModelView: PlanetaryModelView!
    override func viewDidLoad() {
        func size(_ size: CGFloat) -> CGSize {
            return CGSize(width: size, height: size)
        }

        // Create planets
        let planet1 = Planet(image: #imageLiteral(resourceName: "planet1.png"), speed: 75, offset: 10, size: size(10), orbit: 0, rotateClockwise: true)
        let planet2 = Planet(image: #imageLiteral(resourceName: "planet2.png"), speed: 35, offset: 20, size: size(30), orbit: 1, rotateClockwise: true)
        let planet3 = Planet(image: #imageLiteral(resourceName: "planet3.png"), speed: 35, offset: 40, size: size(33), orbit: 1, rotateClockwise: true)
        let planet4 = Planet(image: #imageLiteral(resourceName: "planet4.png"), speed: 35, offset: 60, size: size(35), orbit: 2, rotateClockwise: true)
        let planet5 = Planet(image: #imageLiteral(resourceName: "planet5.png"), speed: 20, offset: 40, size: size(22), orbit: 2, rotateClockwise: true)
        let planet6 = Planet(image: #imageLiteral(resourceName: "planet6.png"), speed: 10, offset: 80, size: size(23), orbit: 3, rotateClockwise: true)

        // Add planets to the planetary model
        self.planetaryModelView.add(planet: planet1)
        self.planetaryModelView.add(planet: planet2)
        self.planetaryModelView.add(planet: planet3)
        self.planetaryModelView.add(planet: planet4)
        self.planetaryModelView.add(planet: planet5)
        self.planetaryModelView.add(planet: planet6)

        // Create the Sun
        let sun = Sun(image: #imageLiteral(resourceName: "sun.png"), speed: 80, size: size(50), rotateClockwise: true)

        // Add sun to planetary model
        self.planetaryModelView.add(sun: sun)
    }

    override func viewDidLayoutSubviews() {
        // Start animation when all views finished with auto layout
        self.planetaryModelView.animation(play: true)
    }
}

There you have it. The planetary model is complete. You can play with fancy assets and some different properties to create your own awesome animations.

We are Creitive, digital product agency. If you want to learn more about animations, check out our Product Development Services

Insights

Explore all