Mathew Sanders /

Animations in Swift (pt 2)

In Prototyping iOS Animations in Swift I introduced the a range of block-based functions that UIKit provides to create tweened animations and how a simple animation can be programmatically altered with some random variation to create more complex scenes.

This alone can create an interesting range of animations, but is still only the tip of the iceberg of what Apple provides as tools for creating animations.

This tutorial looks at some more animation functions that require a little bit more setting up to create, but once mastered opens up an even larger number of possibilities.

Topics

Container view transitions

If you want to perform an animated transition between two views, Apple provides a handful of default animations that are easy to create with just a small bit of code.

The trick to using these methods is that the transition needs to be performed in a parent container, which is typically just an invisible UIView that’s the size of the largest object you’re transitioning with. So performing this animation requires a little bit of setting up.

For example, to animate a transition between two colored UIViews we’ll use a third UIView as the container for the animation.

Lets set up all three views in the viewDidLoad() function…

let container = UIView()
let redSquare = UIView()
let blueSquare = UIView()

override func viewDidLoad() {
    super.viewDidLoad()
    
    // set container frame and add to the screen
    self.container.frame = CGRect(x: 60, y: 60, width: 200, height: 200)
    self.view.addSubview(container)
    
    // set red square frame up
    // we want the blue square to have the same position as redSquare 
    // so lets just reuse blueSquare.frame
    self.redSquare.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
    self.blueSquare.frame = redSquare.frame
    
    // set background colors
    self.redSquare.backgroundColor = UIColor.redColor()
    self.blueSquare.backgroundColor = UIColor.blueColor()
    
    // for now just add the redSquare
    // we'll add blueSquare as part of the transition animation 
    self.container.addSubview(self.redSquare)   
}

Running the app now all you should see is a redSquare:

Now lets add an @IBAction from a button in the storyboard to trigger the actual animation and use the function UIView.transitionWithView which takes these options:

  • view: the view that the transition is animated within
  • duration: the number of seconds the transition will take
  • options: options for the transition (e.g. the animation style to use)
  • animations: a block defining changes to make as part of the transition; and
  • completion: another block this one defining code to run when the animation has completed.

The animation block should typically include the removal of one view, and the addition of the view (to the container) that is replacing it.

@IBAction func animateButtonTapped(sender: AnyObject) {
  
	// create a 'tuple' (a pair or more of objects assigned to a single variable)
	let views = (frontView: self.redSquare, backView: self.blueSquare)

	// set a transition style
	let transitionOptions = UIViewAnimationOptions.TransitionCurlUp

	UIView.transitionWithView(self.container, duration: 1.0, options: transitionOptions, animations: {
	    // remove the front object...
	    views.frontView.removeFromSuperview()
   
	    // ... and add the other object
	    self.container.addSubview(views.backView)
   
	}, completion: { finished in
	    // any code entered here will be applied
	    // .once the animation has completed
	})
}

The transition from red square to blue square works as expected, but after that we’re stuck with the blue square.

This is because our @IBAction function is set up with the expectation that the redSquare is the visible square, and this is only true the first time we tap the animate button.

To fix this we’ll need to add some conditional logic that checks to see which square is currently visible to figure out if we should the transitioning from red-to-blue, or blue-to-red.

There are lots of ways this could be done, but lets use a feature of Swift called a ‘tuple’1.

// create a 'tuple' (a pair or more of objects assigned to a single variable)
var views : (frontView: UIView, backView: UIView)

// if redSquare has a superView (e.g it's in the container)
// set redSquare as front, and blueSquare as back
// otherwise flip the order
if(self.redSquare.superview){
    views = (frontView: self.redSquare, backView: self.blueSquare)
}
else {
    views = (frontView: self.blueSquare, backView: self.redSquare)
}

Now we can alternate between the red and blue views!

Switching from one view to another is such a common task that so long as we don’t have anything else we also want to animate with the transition, Apple provides a slightly easier function that does the removeFromSuperview() and addSubView() for you automatically.

Here’s our simplified @IBAction function using this simplified transition function:

@IBAction func animateButtonTapped(sender: AnyObject) {
    
    // create a 'tuple' (a pair or more of objects assigned to a single variable)
    var views : (frontView: UIView, backView: UIView)

    if(self.redSquare.superview){
        views = (frontView: self.redSquare, backView: self.blueSquare)
    }
    else {
        views = (frontView: self.blueSquare, backView: self.redSquare)
    }
    
    // set a transition style
    let transitionOptions = UIViewAnimationOptions.TransitionCurlUp

    // with no animation block, and a completion block set to 'nil' this makes a single line of code  
    UIView.transitionFromView(views.frontView, toView: views.backView, duration: 1.0, options: transitionOptions, completion: nil)
    
}

Now let’s try out some of the different default transition options that are available:

let transitionOptions = UIViewAnimationOptions.TransitionCurlDown

let transitionOptions = UIViewAnimationOptions.TransitionFlipFromLeft

Keyframe block animations

Another new addition in iOS 7, instead of creating an animation by interpolating between a start value to an end value, this method allows us to define an by as many sub-parts as we want.

For one example of why you’d want to do this, consider how you would rotate an image a full 360 degrees.

Using our familiar basic animation functions you could attempt to animate the transform property like so:

// create and add blue-fish.png image to screen
let fish = UIImageView()
fish.image = UIImage(named: "blue-fish.png")
fish.frame = CGRect(x: 50, y: 50, width: 50, height: 50)
self.view.addSubview(fish)

// angles in iOS are measured as radians PI is 180 degrees so PI × 2 is 360 degrees
let fullRotation = CGFloat(M_PI * 2)

UIView.animateWithDuration(1.0, animations: {
    // animating `transform` allows us to change 2D geometry of the object 
    // like `scale`, `rotation` or `translate`
    self.fish.transform = CGAffineTransformMakeRotation(fullRotation)
})

But iOS can’t interpolate between the start and end values because they are equivalent!

To get around this, we’ll use animateKeyframesWithDuration to define the rotation in smaller parts, which iOS won’t get confused with, and then combine them all into a single animation.

To do the full rotation, lets break the animation into three parts, with each part rotating one third2 of the way around:

let duration = 2.0
let delay = 0.0
let options = UIViewKeyframeAnimationOptions.CalculationModeLinear

UIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {
    // each keyframe needs to be added here
    // within each keyframe the relativeStartTime and relativeDuration need to be values between 0.0 and 1.0
    
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1/3, animations: {
        // start at 0.00s (5s × 0)
        // duration 1.67s (5s × 1/3)
        // end at   1.67s (0.00s + 1.67s)
        self.fish.transform = CGAffineTransformMakeRotation(1/3 * fullRotation)
    })
    UIView.addKeyframeWithRelativeStartTime(1/3, relativeDuration: 1/3, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(2/3 * fullRotation)
    })
    UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(3/3 * fullRotation)
    })
    
    }, completion: {finished in
        // any code entered here will be applied
        // once the animation has completed
    
    })
}

Now iOS has enough information to create the animation as we expected.

If you’re manually entering values for relativeStartTime & relativeDuration it’s easy to make a mistake, but if all you want to achieve is a smooth transition between each of the keyframes, you can enter CalculationModePaced as an option which ignores any values you’ve entered for relativeStartTime and relativeDuration and automatically figures out correct values for a consistent animation:

let duration = 2.0
let delay = 0.0
let options = UIViewKeyframeAnimationOptions.CalculationModePaced

UIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {
    
    // note that we've set relativeStartTime and relativeDuration to zero. 
    // Because we're using `CalculationModePaced` these values are ignored 
    // and iOS figures out values that are needed to create a smooth constant transition
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(1/3 * fullRotation)
    })
    
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(2/3 * fullRotation)
    })
    
    UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 0, animations: {
        self.fish.transform = CGAffineTransformMakeRotation(3/3 * fullRotation)
    })
    
}, completion: nil)

Moving an object along a bezier curve

A really fun animation to create is move the position of an object along a curve.

Our basic animation techniques make it easy to animate an object moving from point A to B, but to have an object move along the multiple points of a curve A,B,C,D,E we’ll need to use a keyframe-based animation again.

You could do this manually using the keyframe block function like we did for the rotation animation, but to get a nice smooth animation we’d have to define a lot of keyframes and it would get quickly get very complicated and messy.

Luckily, instead of assigning each keyframe manually, we can give iOS a bezier curve3 and the keyframes needed will be generated automatically.

This requires us to use more powerful animation features of iOS that are slightly more complicated, but not too hard once you get the general approach.

// first set up an object to animate
// we'll use a familiar red square
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)
square.backgroundColor = UIColor.redColor()

// add the square to the screen
self.view.addSubview(square)

// now create a bezier path that defines our curve
// the animation function needs the curve defined as a CGPath
// but these are more difficult to work with, so instead
// we'll create a UIBezierPath, and then create a 
// CGPath from the bezier when we need it
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 16,y: 239))
path.addCurveToPoint(CGPoint(x: 301, y: 239), controlPoint1: CGPoint(x: 136, y: 373), controlPoint2: CGPoint(x: 178, y: 110))

// create a new CAKeyframeAnimation that animates the objects position 
let anim = CAKeyframeAnimation(keyPath: "position")

// set the animations path to our bezier curve
anim.path = path.CGPath

// set some more parameters for the animation
// this rotation mode means that our object will rotate so that it's parallel to whatever point it is currently on the curve 
anim.rotationMode = kCAAnimationRotateAuto
anim.repeatCount = Float.infinity
anim.duration = 5.0

// we add the animation to the squares 'layer' property
square.layer.addAnimation(anim, forKey: "animate position along path")

Now we have a single animation, lets use it multiple times to create a more complex scene.


// loop from 0 to 5
for i in 0...5 {
    
    // create a square 
    let square = UIView()
    square.frame = CGRect(x: 55, y: 300, width: 20, height: 20)
    square.backgroundColor = UIColor.redColor()
    self.view.addSubview(square)
    
    // randomly create a value between 0.0 and 150.0
    let randomYOffset = CGFloat( arc4random_uniform(150))
    
    // for every y-value on the bezier curve
    // add our random y offset so that each individual animation
    // will appear at a different y-position
    let path = UIBezierPath()
    path.moveToPoint(CGPoint(x: 16,y: 239 + randomYOffset))
    path.addCurveToPoint(CGPoint(x: 301, y: 239 + randomYOffset), controlPoint1: CGPoint(x: 136, y: 373 + randomYOffset), controlPoint2: CGPoint(x: 178, y: 110 + randomYOffset))
    
    // create the animation 
    let anim = CAKeyframeAnimation(keyPath: "position")
    anim.path = path.CGPath
    anim.rotationMode = kCAAnimationRotateAuto
    anim.repeatCount = Float.infinity
    anim.duration = 5.0
    
    // add the animation 
    square.layer.addAnimation(anim, forKey: "animate position along path")
}

Now we have multiple squares animating, but they all start at the same time so don’t look so great.

Lets set some more properties to the animation object that adds some randomness to how long the animation takes (so that some of the squares will move faster than others), and at what position of the animation it starts (so that they appear staggered).

// each square will take between 4.0 and 8.0 seconds
// to complete one animation loop
anim.duration = Double(arc4random_uniform(40)+30) / 10

// stagger each animation by a random value
// `290` was chosen simply by experimentation
anim.timeOffset = Double(arc4random_uniform(290))

Now our squares follow a bezier curve with with much more variation due to the randomness we’ve added to each animation.

From here, it’s not too much of a step to switch the redSquares to images and add a background to create an interesting organic animation like a school of fish or a flock of birds4.

Animating appearance of a bezier curve

Another useful technique to know that also uses an curve it to animate how much of the curve is drawn.

When we animated an object along a curve, the bezier path wasn’t actually shown on the screen, instead it was used as an input to the keyframe animation.

In this example, we’ll actually draw the curve to the screen, but animate how much of the curve is shown from 0 to 100%.

Lets add this code into an @IBAction function that’s triggered when a button is tapped.

// set up some values to use in the curve
let ovalStartAngle = CGFloat(90.01 * M_PI/180)
let ovalEndAngle = CGFloat(90 * M_PI/180)
let ovalRect = CGRectMake(97.5, 58.5, 125, 125)

// create the bezier path
let ovalPath = UIBezierPath()

ovalPath.addArcWithCenter(CGPointMake(CGRectGetMidX(ovalRect), CGRectGetMidY(ovalRect)),
    radius: CGRectGetWidth(ovalRect) / 2,
    startAngle: ovalStartAngle,
    endAngle: ovalEndAngle, clockwise: true)

// create an object that represents how the curve 
// should be presented on the screen
let progressLine = CAShapeLayer()
progressLine.path = ovalPath.CGPath
progressLine.strokeColor = UIColor.blueColor().CGColor
progressLine.fillColor = UIColor.clearColor().CGColor
progressLine.lineWidth = 10.0
progressLine.lineCap = kCALineCapRound

// add the curve to the screen
self.view.layer.addSublayer(progressLine)

// create a basic animation that animates the value 'strokeEnd'
// from 0.0 to 1.0 over 3.0 seconds
let animateStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
animateStrokeEnd.duration = 3.0
animateStrokeEnd.fromValue = 0.0
animateStrokeEnd.toValue = 1.0

// add the animation
progressLine.addAnimation(animateStrokeEnd, forKey: "animate stroke end animation")

This animation uses a very simple oval as a curve, but this technique can be applied to any curve at all. I’ve seen examples where someone has converted cursive text to create the illusion of a word being written, or you could combine it with animating an object along the same curve to show the path it’s taken while animating.

System default animation(s)

Another addition with iOS 7 is UIView.performSystemAnimation which for now only has UISystemAnimation.Delete as a valid option but I’m hoping that in the future Apple will add more standard system animations that can be easily created using this function.

// create and add blue-fish.png image to screen
let fish = UIImageView()
fish.image = UIImage(named: "blue-fish.png")
fish.frame = CGRect(x: 50, y: 50, width: 50, height: 50)
self.view.addSubview(fish)

// create an array of views to animate (in this case just one)
let viewsToAnimate = [fish]

// perform the system animation
// as of iOS 8 UISystemAnimation.Delete is the only valid option
UIView.performSystemAnimation(UISystemAnimation.Delete, onViews: viewsToAnimate, options: nil, animations: {
    // any changes defined here will occur
    // in parallel with the system animation 

}, completion: { finished in 
    // any code entered here will be applied
    // once the animation has completed
       
})

Fin

Thank you Paul Webb for catching my bad Fin/fish joke in my last post5.

These animations start to get a little more complex so you might not succeed the first time around, but if you’re ever stuck, post a link to your code and I’d be happy to try and help!

Read More Swift Animation Posts...
Animations
Part 1
Transitions
Part 1
Transitions
Part 2
Transitions
Part 3

Notes

  1. Tuples are one of the reasons I love another programming language called Haskell. Most programming languages are designed so that a variable contains a single object. Tuples allow us to create an ad-hoc data structure that contains two (or more) objects of our choice.

  2. Splitting the animation into two halves is still potentially ambiguous since the rotation (in degrees) from 0 to 180 could equally be achieved either by moving clockwise or anti clockwise. It might work to define the rotation as (0 to 180 then 180 to 360) but since 0 and 360 are equivalent you might end up with the rotation backtracking on itself.

  3. Making bezier curves from control points isn’t easy to do if you’ve never worked with them outside of vector software before :(

    If you want to do it manually, the best place to start would be David Rönnqvist’s post: Thinking like a bezier path.

    Or, if you’re like me and that’s just too much, download PaintCode which (among other useful things) will automatically create Swift code for a bezier curve you’ve drawn on screen.

  4. For this animation I also added a random size for each fish, but instead of randomly adding variation to the speed of each fish I calculated the duration of each animation based off the size so that smaller fish take longer to complete one loop than the larger fish. This helps create an illusion of depth and perspective since closer objects should appear to move faster.

  5. For the record, I’m not a French New Wave fan: sadly it was a reference to Sharknado.