Mathew Sanders /

Prototyping Animated Transitions in Swift

In previous posts I introduced some simple animation techniques in Swift.

This post looks at how to you can extend these animation techniques to create your own custom transitions like this…

…instead of relying on the handful of default animations that Apple provides1. In the next I look at a more real life example of an animated menu.

TL;DR

If you want to jump straight in you can download this sample project2 which includes a basic storyboard with two screens connected to each other, and a transition that animates by rotating the screens from the top-left point of the window.

Or, you can follow with me step-by-step for a better understanding of what’s happening.

Topics

Create a simple project

Lets start from scratch and create a simple storyboard project with two view controllers that we can navigate between by tapping on a button.

1. Create a new project

Use the ‘iOS Single View template’ which automatically creates a project with a storyboard and a single view controller.

2. Add a button to the screen

Also add constraints so that the button is fixed to the bottom, left and right edges of the screen by 20pts.

3. Duplicate the screen

Just like any other editor, in Xcode you can ⌘-C to copy and ⌘-P to paste objects in your storyboard.

4. Create a segue between the screens

Option-click on a button on one screen and drag over to the other screen and release to create a segue.

Pick ‘custom’ when promoted to choose a type of segue.

5. Update the second screen

Change the button text to read ‘Hide’ and change the color of the background and button text.

6. Run the project!

By now you should be able to run the project and tap on the Show button to present the second screen2.

Because we’ve not specified any information for the transition yet, iOS defaults to the default transition animation of the second screen sliding up from the bottom.

Tapping on the Hide button does nothing yet. So the next step is to fix that…

Aside: Segues & Exit Segues

When we present a view controller in iOS with a segue it may seem like we’re replacing that screen, but actually we’re just adding a new screen onto a stack of screens in which only the last screen in that stack is visible.

If it’s a new concept it can be tricky at first to understand so lets take a closer look.

Consider a two screens that are connected to each other with two segues that we’ll call “Blue” and “Red”. Segue Blue presents the dark colored screen and Segue Red presents the light colored screen.

If we start the app on the light colored screen and perform the Blue segue an instance of the dark colored screen is added to the stack of screens and we see the dark screen because it’s on top.

But if we continue perfuming segues we just end up adding more and more screens onto our stack of view controllers.

It may seem like we’re replacing the screens with each segue because all we see is the screen that’s on the top of the stack but with each segue the stack is growing larger and larger.

Eventually, if this is not handled the app will eventually crash because each screen requires memory to be allocated to it and there is always finite amount of memory to use.

Instead of performing a regular segue to attempt to return to an earlier screen we need to create an exit segue that allows us to unwind to an earlier screen in the stack.

7. Attempt Exit segue

Creating an exit segue is similar to a regular segue in that you start by selecting the object you want to trigger to segue but instead of dragging to another view controller you control-click and drag and release on the screens exit segue icon.

We can try that now…

… but nothing will happen when we release the drag because we need to do a little setting up first.

8. Create an exit point

To create an exit segue, we need to create at least one IBAction in the view controller file that we want to return to.

We can choose any name we want for the function, but it needs to be an @IBAction and take a UIStoryboardSegue as a parameter.

Here’s the method that I created. Note that to perform the exit segue the method just needs to exist, we don’t need to have any code in the function body.

@IBAction func unwindToViewController (sender: UIStoryboardSegue){

}

Now when we return to our storyboard file and drag onto the exit segue button a pop up menu gives us an option to connect the IBAction we just created.

9. Run the project again

Now when we run the project2, tapping the Hide button removes the screen that’s currently on the stack and shows us the screen that’s below.

Custom Transition Animations

Now that we have a simple project with the default transitions between two screens, how do we go about creating a custom transition animation?

General approach

The general approach is to create an “animator” object that is responsible for every type of animation we want to show.

Then we need to tell iOS when those animator objects should be used instead of using a default transition.

Both of these are tasks are achieved by using a pattern object-oriented programming pattern called protocols3.

To create the animator we need an object that adheres to the UIViewControllerAnimatedTransitioning protocol and to tell iOS when those animators should be used we need an object that adheres to the UIViewControllerTransitioningDelegate protocol.

There are multiple ways that this could be achieved, but the approach that I’m going to show you is to create an object that will meet the requirements of both of these protocols. We’ll call it a TransitionManager.

1. Create the TransitionManager class

Create a new file in your project by pressing ⌘-N and choose ‘Cocoa Touch Class’ in the first dialog and enter the class TransitionManager and subclass NSObject.

This will create a new Swift file that looks something like this2:

//
//  TransitionManager.swift
//  Transition
//

import UIKit

class TransitionManager: NSObject {
   
}

2. Enter protocol methods

To meet the requirements of these two protocols3, our TransitionManager class needs to implement a total of four methods.

The first two methods meet the requirements of being an animator:

  • animateTransition defines the animations that transition from one screen to another.
  • transitionDuration returns the number of seconds that the animation takes.

The final two meet the requirements of being the “transition delegate” by returning which animator should be used for either presenting or dismissing a screen:

  • animationControllerForPresentedController
  • animationControllerForDismissedController

Here’s the updated TransitionManager class2 with these four methods completed except for the animateTransition which we still have to complete.

//
//  TransitionManager.swift
//  Transition
//

import UIKit

class TransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate  {
 
    // MARK: UIViewControllerAnimatedTransitioning protocol methods
    
    // animate a change from one viewcontroller to another
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        // TODO: Perform the animation
    }
    
    // return how many seconds the transiton animation will take
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return 0.5
    }
    
    // MARK: UIViewControllerTransitioningDelegate protocol methods
    
    // return the animataor when presenting a viewcontroller
    // remmeber that an animator (or animation controller) is any object that aheres to the UIViewControllerAnimatedTransitioning protocol
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }
    
    // return the animator used when dismissing from a viewcontroller
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return self
    }
    
}

3. Complete animateTransition method

Lets complete the animateTransition function to perform an transition where the new screen slides in from the right as the old screen slides off to the left.

Here’s the animateTransition method completed to do this:

// animate a change from one viewcontroller to another
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        // get reference to our fromView, toView and the container view that we should perform the transition in
        let container = transitionContext.containerView()
        let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
        let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
        
        // set up from 2D transforms that we'll use in the animation
        let offScreenRight = CGAffineTransformMakeTranslation(container.frame.width, 0)
        let offScreenLeft = CGAffineTransformMakeTranslation(-container.frame.width, 0)
        
        // start the toView to the right of the screen
        toView.transform = offScreenRight
        
        // add the both views to our view controller
        container.addSubview(toView)
        container.addSubview(fromView)
        
        // get the duration of the animation
        // DON'T just type '0.5s' -- the reason why won't make sense until the next post
        // but for now it's important to just follow this approach
        let duration = self.transitionDuration(transitionContext)
        
        // perform the animation!
        // for this example, just slid both fromView and toView to the left at the same time
        // meaning fromView is pushed off the screen and toView slides into view
        // we also use the block animation usingSpringWithDamping for a little bounce
        UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.8, options: nil, animations: {
            
            fromView.transform = offScreenLeft
            toView.transform = CGAffineTransformIdentity
            
            }, completion: { finished in
                
                // tell our transitionContext object that we've finished animating
                transitionContext.completeTransition(true)
                
        })
        
    }

The important parts to notice are:

  • iOS passes us an object called transitionContext whenever this method is performed. This object gives us references to views of screens that we’re transitioning from and to and very importantly gives us a third view that acts as a container for the animations to be performed in.
  • We have to manually add our fromView and toView to the container view. The order we add our views determines which will be shown on top if they overlap as part of the animation.
  • We use a regular block animation to perform the animation, but instead of entering the animation duration we call the method self.transitionDuration(transitionContext).
  • When we’ve completed the animation we call the method transitionContext.completeTransition(true).

Before we run the project we need to update our ViewController class and create an instance of the our TransitionManager object.

// add this right above your viewDidLoad function...
let transitionManager = TransitionManager()

And add a prepareForSegue method which is called whenever iOS is preparing to transition from one screen to another. This is a good point to tell iOS that we want to use our transition manager instead of a default transition animation.

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    
        // this gets a reference to the screen that we're about to transition to
        let toViewController = segue.destinationViewController as UIViewController
        
        // instead of using the default transition animation, we'll ask
        // the segue to use our custom TransitionManager object to manage the transition animation
        toViewController.transitioningDelegate = self.transitionManager
        
    }

Now when we run the project2 you should see a slide transition instead of the default transition.

But the animation is the exactly the same for both presenting and dismissing the screen!

4. Reversing the transition

To reverse the transition we just need to add a little logic to our TransitionManager to keep track of if we’re presenting a screen or dismissing a screen and update the animation accordingly.

To do this, lets add a variable to our TransitionManager class called presenting.

In the animationControllerForPresentedController set presenting to be true, and in animationControllerForDismissedController set it to be false.

Then in the animateTransition method check to see if presenting is true or false and update the animation accordingly.

Try this on your own, if you get stuck, here’s a diff that looks at the changes I made to update TransitionMaster at this step, or you can download the entire updated project2.

Running the project with these updates the transition animation should now reverse when dismissing a screen.

5. A more complex animation

Now lets update the transformations in animateTransition so that instead of the screen sliding in, they rotate in. Here’s are the most important changes to the method:

// set up from 2D transforms that we'll use in the animation
let π : CGFloat = 3.14159265359

let offScreenRotateIn = CGAffineTransformMakeRotation(-π/2)
let offScreenRotateOut = CGAffineTransformMakeRotation(π/2)

// set the start location of toView depending if we're presenting or not
toView.transform = self.presenting ? offScreenRotateIn : offScreenRotateOut

// set the anchor point so that rotations happen from the top-left corner
toView.layer.anchorPoint = CGPoint(x:0, y:0)
fromView.layer.anchorPoint = CGPoint(x:0, y:0)

// updating the anchor point also moves the position to we have to move the center position to the top-left to compensate
toView.layer.position = CGPoint(x:0, y:0)
fromView.layer.position = CGPoint(x:0, y:0)

Running the project now2 you should see a transition with the screens rotating in and out instead of a slide.

Note that with this animation there are times where only a small part of each screen is visible and we see a black background underneath. This is the default background color of our container view and can be updated if needed.

7. Updating the status bar style

For a final touch lets override the preferredStatusBarStyle method in our ViewController class so that the status bar style switches from default to light depending if we’re presenting a screen or not.

// we override this method to manage what style status bar is shown
    override func preferredStatusBarStyle() -> UIStatusBarStyle {
        return self.presentingViewController == nil ? UIStatusBarStyle.Default : UIStatusBarStyle.LightContent
    }

Next steps

Hopefully this has given you a good starting point for creating your own custom animated transitions.

The samples I’ve given have created a transition that only uses the views of the screens that we’re transitioning from and to, but remember that the container view that the animation is performed in is just a regular UIView and we can add anything we want to it.

Also, the animations have all been 2D transforms but 3D transforms can be created when you animate the views layer object.

We’ve looked specifically at a modal transition, there are some differences when creating a transition for a push navigation or a changing screens in a tab bar but the general approach is the same.

The next post uses this technique to create an animated menu.

Soon I’m going to write about creating interactive transitions (where a transition completes interactively with a gesture), and transitions that have visual continuity between screens (for example an element from the first screen that persists to the next).

Follow me at @permakittens to get updates on new posts!

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

Notes

  1. Apple has created a solid visual language for transitions between screens. Make sure that you respect the semantic meanings of these transitions and you’re not adding chaining these defaults just for a ‘wow’ effect. 

  2. If you’re having trouble at any point, here are versions of the project at each step:

    Note: Sample code has been made in Xcode Beta-7 release and won’t work in some earlier versions.  2 3 4 5 6 7 8

  3. Protocols are a technique in object-oriented programming that allow a sensible way to break up code into more maintainable and reusable objects. Protocols work by allowing us to assign responsibility to parts of code without knowing the underlying implementation of the code.

    For instance, if the analogy of protocols were applied to humans, a person could declare that she adheres to the ‘I-can-take-you-some-where’ protocol which is defined by two commands: pickUpFrom(aLocation) and deliverTo(anotherLocation).

    Anyone who declares that they adhere to that protocol are guaranteeing that they can perform those two commands. But at the same time isn’t giving away how those commands may her performed. For instance a person may walk, run, drive or fly a plane to meet the requirements of those commands.  2