Custom Segues for Fun and Profit
March 16, 2015 -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:
- sourceViewController: This is the view controller that is doing the presenting. It has no type information, so you'll need to typecast it in Swift. So far I have tried to only cast it as UIViewController and not a specific view controller. This way your segue doesn't only work for a specific view controller subclass.
- destinationViewController: This is the view controller you're transitioning to. It shares the same type concerns as sourceViewController.
- identifier: This is so the rest of your app can see this segue coming. Your app will never manually create an instance of your custom segue, so this identifier is the only way your app will be able to find it. You probably won't need to do anything with this in your transition code.
- Additional properties: Since you're making a subclass, you can add your own properties.This gets a bit tricky, but sometimes this might be necessary. Use with caution.
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.