Celebrating 10 Years!

profile picture

Custom Segues for Fun and Profit

March 16, 2015 - Roundwall Software

With new devices like the iPhone 6+, even naysayers are considering using Storyboards. Adopting Storyboards, even for a portion of your app, means easy access to plenty of handy features such as Size Classes, Auto-Layout, and my new friend: custom animations in view controller transitions.

Now you can technically make custom transitions without Storyboards. For example, the UINavigationControllerDelegate protocol defines a number of methods you can implement for custom transitions between view controllers when using UINavigationController's -pushViewController:animated:. With storyboards, we have a simpler interface that works for all transitions without needing a different kind of implementation for each situation.

I have an example project to demonstrate. Let's walk through what's going on:

Setup

First you'll need to be using a Storyboard for at least part of your app. This example app just has two view controllers. Each has a button, one to trigger the segue, and one to trigger an exit segue to come back. This commit shows the project like this. Always good to commit before you start with the crazy stuff.

Baby Steps

First you'll need a subclass of UIStoryboardSegue. I called mine CustomSegue because I'm trying to explain a concept here, not be super creative. Your subclass needs to implement one single method, -perform. It is responsible for doing whatever you want to make your UI look amazing. It is also responsible for making sure that your final controller's UI is on the screen. There are some caveats to be aware of, so let's take take this in small steps.

UIStoryboardSegue has a few properties:

With all that information, here's my first custom subclass:

class CustomSegue: UIStoryboardSegue {
    override func perform() {
        let source = sourceViewController as UIViewController
        let destination = destinationViewController as UIViewController
        
        source.presentViewController(destination, animated: true, completion: nil)
    }
}

We don't even do anything special! We just grab each view controller and use UIKit's existing code to do the hard work for us. Now you know how your segue was working before you provided this custom subclass. If this was all you needed, you can stop here.

I'm going to assume you wanted something more than that, that's why you're reading an article about custom segues. Let's do that!

class CustomSegue: UIStoryboardSegue {
    override func perform() {
        let source = sourceViewController as UIViewController
        let destination = destinationViewController as UIViewController
        let window = UIApplication.sharedApplication().keyWindow!
        
        destination.view.alpha = 0.0
        window.insertSubview(destination.view, belowSubview: source.view)
        
        UIView.animateWithDuration(0.5, animations: { () -> Void in
            source.view.alpha = 0.0
            destination.view.alpha = 1.0
        }) { (finished) -> Void in
            source.view.alpha = 1.0
            source.presentViewController(destination, animated: false, completion: nil)
        }
    }
}

Now we've added a few things here. First we need to set the destination view's alpha to invisible and insert the destination view under the source's view. Without inserting the destination view, our animation would be fading the source controller into a black screen, not into our destination view.

Then we can animate our crossfade. We fade the source out to invisible and fade the destination into fully visible. Crossfade!

Finally we need to restore everything to the way it should be when our transition is done. We still use that -presentViewController:animated:completion: method, but this time without animation. This is to ensure our view controller hierarchy is correct when this is done. We also need to make sure we set the source view's alpha back to full. In early attempts to implement this, I left this tiny bit out and was thoroughly confused. Each time I tested the segue, presentation would work fine, but the exit would take me to a black screen. This is because the source view was still invisible. Save yourself the confusing and make sure you reset any changes you make to the source controller's view.

Enjoy!

From here you're free to go nuts with all the transforms, animations, and dynamics you want to use to make your app unique and amazing. The sky is the limit! If your views are especially complicated, you might find methods like - snapshotViewAfterScreenUpdates: helpful.