Elegant Custom UIViewController Transitions

Tim Moose Tutorials 16 Comments

If you’ve ever implemented a custom view controller transition by adopting UIViewControllerTransitioningDelegate and UIViewControllerContextTransitioning in your view controller, you may have found that the result added unwelcome clutter to your view controller and wasn’t very reusable. This tutorial will show you how to build a reusable, standalone view controller transition by subclassing UIStoryboardSegue. You’ll see how the segue conveniently integrates with Interface Builder as a custom segue type and can be used in any UIKit app — no storyboard required.

A SwiftMessages custom segue demo

The image above shows this technique in action in the SwiftMessages demo app. The transitions you see where done using a custom segue that I introduced in SwiftMessages 5 to utilize SwiftMessages layouts, animations, and more for modal view controller presentation and dismissal.

For this tutorial, we’re going to reproduce the bottom card-style transition. To keep it simple, some of the non-essential functionality, like background dimming and dismissal gestures, will be left to you as an exercise.

The full source of the final project can be found on our CustomSegueDemo GitHub repo. Let’s get started.

Step 1: Create a custom segue.

Start by creating a subclass of UIStoryboardSegue called BottomCardSegue.

The segue’s action happens inperform(), which we override and do a modal presentation.

At this stage, BottomCardSegue is already a fully-functional custom segue. It just doesn’t do anything interesting yet.

Step 2: Set up the storyboard.

Taking a look at main.storyboard, the initial view controller consists of a “Show” and “Show Programmatically” buttons. The destination view controller is a simple navigation stack (copied directly from the SwiftMessages demo app). We define our storyboard segue (highlighted blue) using the standard procedure:

  1. Control-drag from “Show” to the navigation controller.
  2. Select “bottom card” from the segue type prompt.

Interface Builder has conveniently included BottomCardSegue in the segue type prompt using the autogenerated (and oddly lower-cased) name “bottom card”.

Step 3: Prepare for segue.

Next, we add a “Done” button to the root view controller. I would normally recommend having the “Done” button trigger an unwind segue. However, in order to point out that a custom segue experiences the same lifecycle as a built-in segue, we’ll add the button and configure its action in prepare(for:sender:).

The dismissal action is done in the hide() function by calling dismiss(animated:completion:).

You can now run the app to verify that “Show” and “Done” are working. What you’ll see is the default full-screen modal transition — the one that slides up and down from the bottom.

Configuration Options

Suppose that we added an option to BottomCardSegue to specify whether or not to dim the background. In an ideal world, Interface Builder would support @IBInspectable on UIStoryboardSegue, allowing us to set the dim option directly on the segue’s Attribute Inspector panel. Alas, this is not the world we live in, so I’ll suggest a couple of for configuring segues.

  1. Subclass BottomCardSegue and configure options in init(identifier:source:destination:). For example, we could define BottomCardDimmedSegue with the hypothetical background dimming option enabled. Interface Builder would add a “bottom card dimmed” option to the segue selection prompt.
  2. Downcast segue in prepare(for:sender:) to BottomCardSegue and configure options there.

While you’re considering which path to take, ask Apple to support @IBInspectable on UIStoryboardSegue!

Step 4: Add the custom transition.

We’re ready to add the custom transition by having BottomCardSegue adopt UIViewControllerTransitioningDelegate and UIViewControllerContextTransitioning. We need to add a few more steps to perform() before calling present().

First, set the destination view controller’s transitioning delegate to self to gain control over the transition.

UIKit doesn’t automatically retain instances of UIStoryboardSegue. Therefore, in order to stay around long enough to perform the dismissal transition, we need to create a strong reference to self. We’ll set it to nil after dismissal to avoid leaking memory.

Finally, we set the modal presentation style to .overCurrentContext to allow the presenting view controller to remain visible under the presented content.

Our job as transitioning delegate is to vend the instances of UIViewControllerAnimatedTransitioning that perform the actual presentation and dismissal animations. In a moment, we’ll introduce nested classes Presenter and Dismisser and have each of them adopt UIViewControllerAnimatedTransitioning. But first, let’s adopt UIViewControllerTransitioningDelegate on BottomCardSegue by returning Presenter and Dismisser instances when asked.

The segue’s final task is to vend the dismissal component. So we are free to release the strong self reference just before returning Dismisser().

The Presenter and Dismisser classes adopt UIViewControllerTransitioningDelegate as follows.

We won’t step through the above layout and animation code in detail because transition mechanics are not the focus of this tutorial. Many existing resources do a great job covering UIViewControllerAnimatedTransitioning (try Apple’s documentation, AppCoda, or objc.io).

However, it’s worth noting one layout detail found in Presenter. When setting up the Auto Layout constraints on toView, we respect the view controller’s preferredContentSize.height property by adding an explicit height constraint if preferredContentSize.height >= 0.

No height is specified when preferredContentSize.height == 0 (the default). Instead, Auto Layout is left to work out toView‘s height based on its constraints.

A word of caution about UINavigationController. In our testing, the intrinsic height of a navigation controller does not reflect the Auto Layout constraints of its content. It is therefore necessary to specify a value for preferredContentSize.height. Without it, our navigation stack would have zero height. So let’s specify 200pt in the navigation controller’s Attribute Inspector panel in main.storyboard.

With that out of the way, we’re done! We now have a reusable UIStoryboardSegue subclass that utilizes custom view controller transitioning APIs to present a modal view controller using a card-style layout. Try it out.

Programmatic Transitioning

We’ll conclude by showing how apps that don’t use storyboards can use BottomCardSegue. For simplicity, we’re going to use the view controllers in main.storyboard. The important point is that the transition will be performed programmatically without an associated segue in the storyboard itself.

To set this up, we introduce an @IBAction in the main view controller and connect it to the “Show Programmatically” button.

Instantiate the destination view controller.

Use this to initialize BottomCardSegue.

Call prepare(for:sender:) to configure the “Done” button. I don’t advocate calling this in general, but UIKit doesn’t seem to mind.

Finally, perform the segue.


Here’s what the final result looks like.

The custom segue in action

Custom UIViewController transitions can be a great way make your app unique and enjoyable. We hope this tutorial has inspired you to give it a try. Thanks for reading and happy coding.

Sign up for our newsletter and stay up to date with blog posts, industry news, and more.

Comments 16

    1. Post
    1. Post
    1. Post

      Same as with any other segue: typically, the presented VC would define a delegate and the presenter would set itself as the delegate in prepare(for:sender).

  1. Thanks for this – I’ve always wanted to be able to have the same custom segue that presented a modal handle the dismissal, and it usually involved some parent view controller or another solution that felt hacky. This is a fantastic way to keep UIStoryboardSegue managing the UIViewController stack while allowing the custom segue to handle animations. Retaining self is a great way to ensure that the segue can also handle dismissal.

    One thing I added was to pass the segue as an init param to my presenter class. My segue class has an @objc func unwind(_ sender:Any) method that calls source.dismiss(animated: true, completion: nil) so now if the user taps outside the modal it will dismiss itself using the same transitioningDelegate.

    1. Forgot to mention that I added a UITapGestureRecognizer to allow that outside tap:

      let tgr = UITapGestureRecognizer(target: segue, action: #selector(segue?.unwind))

      1. Post

        I’d be careful. The container view is supplied to you by iOS and I don’t think you can safely assume that it goes away when you dismiss. You don’t want to leave your gesture recognizer hanging around.

        Usually when a background tap dismisses the modal, you dim the background. So maybe add a background dimming view and put your gesture recognizer there?

  2. Thanks for the article. Where to add the pan gesture? ( So you get the reference to the previous view controller)

  3. When setting destination.modalPresentationStyle = .custom, viewWillAppear will not get called upon dismissal not only on the source but any subsequent navigation on a parent navigationController for some odd reason. Setting destination.modalPresentationStyle = .overCurrentContext resolves this issue.

    Unfortunately I have a requirement to add a blue background to cover the view behind it thats why I set it to .custom to trigger presentationController

    1. Post

      The view appearance APIs are only called if the view is removed/removed from the view hierarchy. Therefore, any presentation style that isn’t full screen will not call these methods. If your blue background is opaque, then you should be able to use .overCurrentContext with the technique outlined in the post.

  4. Hi, I found a memory leak problem that destination controller doesn’ ‘t dismiss with animation (code: dismiss(animated: false, completion: nil)). How can i fix the problem?

  5. That’s great. But I want to know how to modify its height when using custom segue,for example when I was using “Bottom Tab”.Thank you!

Leave a Reply

Your email address will not be published. Required fields are marked *