Mathew Sanders /

Transitions in Swift (pt 2)

In the last post I showed how you can create an object to manage a custom transition animation.

That post was limited to animating the entire frame of the screens that are transitioning, but often we want to create transitions where elements within the screen are also animated…

Custom menu transition

…this tutorial takes you step-by-step how to animate elements within the screen as part of your transition.

TL;DR

Like the last post, if you want to jump in here’s a finished sample project1 that’s ready for you to start experimenting with.

But come back after to follow the step-by-step process so you can better understand what’s happening and be able to make your own transitions with confidence.

Topics

Set up your storyboard

For this example we’ll recreate a menu similar to what one of my favorite apps used a few versions ago.

Lay out screens and content

Start by creating a new project and setting up a storyboard with a list screen and a menu screen and laying out your objects and setting sensible constraints.

Create a modal segue between the main screen and the menu screen, and an exit segue2 that unwinds back to the main screen so your storyboard looks something like this:

Layout out elements on storyboards can be time consuming, so if you want just download project with everything set up for this step1.

And when you run the project tapping on the + button should trigger the menu screen with the default modal animation of sliding up from the bottom of the screen.

Add transparency to the menu

The first thing that we’re going to do is add transparency to the menu screen so that we can see the screen below.

Lets try that by updating our storyboard so that the background view menu screen is the color is semi transparent.

When you do this, make sure that you’re adjusting the transparency of the background color and not alpha value of the view3.

Running the project1 now you can see that the background screen is visible while the slide up transition is in progress, but when it ends it disappears and we’re left with a black background.

To fix this we need to make a small adjustment to our modal segue.

With the segue selected, change it’s presentation value from ‘Default’ to ‘Over Full Screen’.

There’s a bug4 with this in iOS where our exit segue no longer performs as expected. If we don’t fix it now tapping the cancel button on our menu screen will have no effect.

Luckily it’s a quick fix. Just add the following line of code in the exit segues @IBAction method to manually dismiss the screen.

@IBAction func unwindToMainViewController (sender: UIStoryboardSegue){
        // bug? exit segue doesn't dismiss so we do it manually... 
        self.dismissViewControllerAnimated(true, completion: nil)
}

When we run the updated project1 you should see that when the modal transition ends the background screen remains visible.

Create a transition manager

Now, lets follow the steps from the last post and create a new object to manage our custom animation for the menu transition.

Simple fade transition

Jump back to the previous tutorial if you’re not familiar with creating a transition manager because the steps here are pretty similar.

Create an simple fade effect by animating the alpha value of the menu screen between 0 to 1. Try this by yourself, but if you get stuck you can always refer to the sample project1.

The main difference from the last post is that in our animateTransition method we want access to individual elements within the menu screen rather than the whole screen.

We won’t do this right now, but to help prepare for this instead of getting the screen views with this method:

transitionContext.viewForKey(UITransitionContextFromViewKey)!

We’re going to get the screens view controllers with this method:

transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!

Also, because our menu view controller will alternate between being the FromViewController and the ToViewController (depending if the method is called as presenting or dismissing) we’re going to use a tuple to hold the view controllers temporarily and then assign menuViewController and bottomViewController depending if our presenting flag is true or false5.

// create a tuple of our screens
let screens : (from:UIViewController, to:UIViewController) = (transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!, transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!)

// assign references to our menu view controller and the 'bottom' view controller from the tuple
// remember that our menuViewController will alternate between the from and to view controller depending if we're presenting or dismissing
let menuViewController = !self.presenting ? screens.from as MenuViewController : screens.to as MenuViewController
let bottomViewController = !self.presenting ? screens.to as UIViewController : screens.from as UIViewController

let menuView = menuViewController.view
let bottomView = bottomViewController.view

Not super exciting, but we’ve set the groundwork for a more complex animation!

Explore sliding onstaging idea

We’ve decided that we want to create an animation where items on the first column slide in from the left, and items in the second column slide in the from the right.

Whenever you’re trying something new it helps to investigate in small steps rather than investing a lot of time trying to get the complete effect all in one step.

So lets try and animate just the two items in the first column: Text and Photo.

Before we animate items within the screen we need some reference to them. Just like we did in the first animation tutorial where we click-dragged from our storyboard onto our swift file to create an @IBAction lets create click-drag from our individual items in the menu screen to create an @IBOutlet for each item we want to animate.

When you’re done, you should have a total of 12 IBOutlets, one for each icon and text label.

@IBOutlet weak var textPostIcon: UIImageView!
@IBOutlet weak var textPostLabel: UILabel!

@IBOutlet weak var photoPostIcon: UIImageView!
@IBOutlet weak var photoPostLabel: UILabel!

@IBOutlet weak var quotePostIcon: UIImageView!
@IBOutlet weak var quotePostLabel: UILabel!

@IBOutlet weak var linkPostIcon: UIImageView!
@IBOutlet weak var linkPostLabel: UILabel!

@IBOutlet weak var chatPostIcon: UIImageView!
@IBOutlet weak var chatPostLabel: UILabel!

@IBOutlet weak var audioPostIcon: UIImageView!
@IBOutlet weak var audioPostLabel: UILabel!

Back to our animateTransition method lets change the animation from just a fade to explore the first two options appearing by sliding in:

First we need to prepare the menu screen for an animation

// setup 2D transitions for animations 
let offstageLeft = CGAffineTransformMakeTranslation(-150, 0)
let offstageRight = CGAffineTransformMakeTranslation(150, 0)

// prepare the menu
if (self.presenting){
    
    // prepare menu to fade in
    menuView.alpha = 0
    
    // prepare menu items to slide in
    menuViewController.textPostIcon.transform = offstageLeft
    menuViewController.textPostLabel.transform = offstageLeft
    
    menuViewController.photoPostIcon.transform = offstageRight
    menuViewController.photoPostLabel.transform = offstageRight
    
}

And then in our animation block

if (self.presenting){
    // fade in
    menuView.alpha = 1
    
    // onstage items: slide in
    menuViewController.textPostIcon.transform = CGAffineTransformIdentity
    menuViewController.textPostLabel.transform = CGAffineTransformIdentity
    
    menuViewController.photoPostIcon.transform = CGAffineTransformIdentity
    menuViewController.photoPostLabel.transform = CGAffineTransformIdentity
    
}
else {
    // fade out
    menuView.alpha = 0
    
    // offstage items: slide out
    menuViewController.textPostIcon.transform = offstageLeft
    menuViewController.textPostLabel.transform = offstageLeft
    
    menuViewController.photoPostIcon.transform = offstageRight
    menuViewController.photoPostLabel.transform = offstageRight
    
}

We’ll either animate the items to their normal position by setting it’s transform property to CGAffineTransformIdentity, or we’ll move them off the edges of the screen with our offstageLeft and offstageRight transforms.

Running the project at this step1 we’ll see the first two items slide in or out with the transition.

Refactor

You might have noticed that with 12 IBOutlets our animation block is going to get quite long. Not only that but in our preparation for the animation and performing the animation we’re actually repeating ourselves.

Developers have a saying called DRY which means Don’t Repeat Yourself and it’s exactly what we’re doing :(

Not a huge deal, but it does mean that in very soon it’s going to slow us down a lot when we want to iterate over a bunch of different options for the animation.

So lets refactor our method so that we have less repeating code.

To do this, we’ll create two new functions

func offStageMenuController(menuViewController: MenuViewController){
    menuViewController.view.alpha = 0

    // setup 2D transitions for animations
    let offstageLeft = CGAffineTransformMakeTranslation(-150, 0)
    let offstageRight = CGAffineTransformMakeTranslation(150, 0)

    menuViewController.textPostIcon.transform = offstageLeft
    // etc for all 12 IBOutlets...
}

func onStageMenuController(menuViewController: MenuViewController){
    // prepare menu to fade in
    menuViewController.view.alpha = 1

    menuViewController.textPostIcon.transform = CGAffineTransformIdentity
    // etc for all 12 IBOutlets...
}

And now our animation block can be simplified so just calling these functions:

// perform the animation!
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: nil, animations: {
    
        if (self.presenting){
            self.onStageMenuController(menuViewController) // onstage items: slide in
        }
        else {
            self.offStageMenuController(menuViewController) // offstage items: slide out
        }

    }, completion: { finished in
        
        // tell our transitionContext object that we've finished animating
        transitionContext.completeTransition(true)
        
        // bug: we have to manually add our 'to view' back http://openradar.appspot.com/radar?id=5320103646199808
        UIApplication.sharedApplication().keyWindow?.addSubview(screens.to.view)
        
})

Running the project with these updates1 we can now see all 12 of our items on the menu screen sliding in or out as part of the transition:

Stagger sliding effect

Everything happening at once doesn’t feel very organic and it would feel more natural to have a staggered effect where each item appears at a different speed to the others.

Thankfully since we’ve refactored our offstaging transitions we only have to make 12 updates instead of 24. But knowing that I’m not going to get the exact transition right at first I’m going to refactor even more.

Lets made a new helper function that takes a number and returns a 2D translation.

func offStage(amount: CGFloat) -> CGAffineTransform {
        return CGAffineTransformMakeTranslation(amount, 0)
}

And in our offstaging helper method we’ll use this

// setup parameters for 2D transitions for animations
let topRowOffset  :CGFloat = 50
let middleRowOffset :CGFloat = 150
let bottomRowOffset  :CGFloat = 300

menuViewController.textPostIcon.transform = self.offStage(-topRowOffset)
menuViewController.textPostLabel.transform = self.offStage(-topRowOffset)

menuViewController.quotePostIcon.transform = self.offStage(-middleRowOffset)
menuViewController.quotePostLabel.transform = self.offStage(-middleRowOffset)

menuViewController.chatPostIcon.transform = self.offStage(-bottomRowOffset)
menuViewController.chatPostLabel.transform = self.offStage(-bottomRowOffset)

menuViewController.photoPostIcon.transform = self.offStage(topRowOffset)
menuViewController.photoPostLabel.transform = self.offStage(topRowOffset)

menuViewController.linkPostIcon.transform = self.offStage(middleRowOffset)
menuViewController.linkPostLabel.transform = self.offStage(middleRowOffset)

menuViewController.audioPostIcon.transform = self.offStage(bottomRowOffset)
menuViewController.audioPostLabel.transform = self.offStage(bottomRowOffset)

Instead of using the same distance in the transform for each item we’ve chunked them into three groups. Because each item animates over the same duration, the items with a shorter distance will appear first with others staggered behind.

Running the project at this step we’ll see this staggered effect.

Swap order of stagger

But I want the bottom to appear first with the middle and top row following so that the effect appears to be coming from the button that triggered the transition.

Luckily, with our refactoring, all we have to do to make this change is swap the values for topRowOffset and bottomRowOffset.

// setup paramaters for 2D transitions for animations
let topRowOffset :CGFloat = 300
let middleRowOffset :CGFloat = 150
let bottomRowOffset :CGFloat = 50

Running the project1 with these updates we’ll see the stagger effect starting with the bottom row and moving to the top.

Next steps

Hopefully you’re excited about prototyping your own animations. Remember that as a designer you don’t have to get the code perfect, but just enough to validate that the animation is possible and works well on an actual device.

Make a friend with someone on your development team and encourage them to help when you get stuck on something specific.

The next post looks at making interactive transitions.

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

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

Notes

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

    Note 1: Sample code has been made in Xcode Beta-7 release and won’t work in some earlier versions. Email me if you’re finding any bugs and I can hopefully point you in the right direction!

    Note 2: In the final Xcode 6 release there was a change in the keyWindow method. It now returns an optional so you’ll need to add the ? to the function

    // will cause an error in Xcode 6
    UIApplication.sharedApplication().keyWindow.addSubview(someView)
    
    // fix for Xcode 6
    UIApplication.sharedApplication().keyWindow?.addSubview(someView) 
    

     2 3 4 5 6 7 8

  2. Jump back to the previous tutorial if you need a refresher on segues & exit segues

  3. Changing the alpha value of the view effects not just the view, but all of the objects in it’s hierarchy. In this case we just want to adjust the background. 

  4. At least I think it’s a bug. For the future it would pay to keep in mind that I might be completely wrong about this! 

  5. We used a similar the exact same technique when looking at container view transitions in my second animation tutorial.