Swifty View Controller Presenters

One major shortcoming of UIKit is that view controllers have too many responsibilities, such as presenting and dismissing view controllers. Jesse Squires discusses how we can re-examine and redefine these common operations with a more Swifty API that reduces boilerplate and increases expressivity.


Introduction (0:00)

View controllers are everywhere. They make up the core of our app, and we couldn’t make apps without them. However, behind every view controller is a presentation controller. In this post, I’ll dig into the presentation controller API and UIKit, using some examples. Then, I’ll wrap it with a more Swifty API to make it nicer to use.

Presentation vs. Transition (01:07)

Transition is about moving from one view controller to another, whereas presentation is about the final appearance, after the transition has completed. They rely on each other, since presentation involves transitioning. In UIKit, the custom presentation API hooks into the custom transitioning API (although this is not required; with custom presentations, you can also provide custom transitions).

Presentation: UIPresentationController (02:06)

Introduced in iOS 8, UIPresentationController APIs are very subtle and usually implicit. Prior to the iPhone 6 and 6S, presentation was only something to be concerned about on the iPad, and less so on the iPhone.

Presentation API: Responsibilities (03:39)

  • Positioning your view in its presentation.
  • Managing the content in the “Chrome”. Content is your view controller’s view, and chrome is everything else outside of that.
  • Adaptation: responding to size class and environment changes, or device rotations.
  • Animation for presenting and dismissing your controller.

Presentation controllers are intended to be reusable. They are not coupled to transitioning delegates or animator objects, other than returning the controller from the transitioning delegate.

3 Phases (05:04)

  1. The presentation phase (moving onscreen)
  2. The management phase (your controller is presented, and you are dealing with possible rotations or other environment changes)
  3. The dismissal (moving offscreen)

Presentation and dismissal are related to transition. You can have custom transitions or you can use default transitions when you are using presentation controllers.

Examples (05:47)

iPad screen presentations diagram

This is a diagram on iPad as a full screen presentation, then a page sheet presentation where the controller is the width of the device. Then, the form sheet presentation as you could see in iOS settings.

Get more development news like this

Also, on iPhone, you have an alert controller action sheets, which is a custom presentation behind the scenes. In iBooks, thre is also a custom presentation controller popover. With the alert controller, we have another presentation controller. That is what’s powering all of these things. (See video above for further examples.)

UIModalPresentationStyle (07:34)

You can set modal presentation styles, such as .Popover, .FullScreen and a few others. UIKit vends a private API class for these different styles, setting these custom presentation controllers for you:

// just set an enum!
self.modalPresentationStyle = .FullScreen
self.modalPresentationStyle = .Popover

Presenting, Dismissing (08:15)

We have four methods to present view controllers (I wish we had just one!). showViewController and showDetailViewController methods are adaptive presentations, mostly used in the context of split view controller (you can now use on iPhone, no longer iPad only). When you cull these methods, in a split view controller it will do different things depending on if you are on iPhone or iPad. presentViewController and pushViewController use the full screen presentation controller.

func presentViewController(_, animated:, completion:)
func pushViewController(_, animated:)
func showViewController(_, sender:)
func showDetailViewController(_, sender:)

We have two dismissing methods, which creates a lopsided API. There are four ways to show, but only two ways to dismiss. push and pop are paired, and the other presentation methods are mapped to the dismiss.

func dismissViewControllerAnimated(_, completion:)
func popViewControllerAnimated(_ animated:) -> UIViewController?

UIPresentationController (10:59)

// Current UIViewController APIs
self.presentationController
self.popoverPresentationController

The UIAlertController was new in iOS 8, replacing UIAlertView and UIActionSheet. It came with a full fledged view controller and custom presentation controllers. The same with UISearchController, which replaced UISearchDisplayController.

UIPopoverPresentationController is explicitly a presentation controller for iPad and iPhone, and it’s a custom controller - you have to set the modal style first. With these changes, there is a more unified, consistent API with regard to view controllers (e.g. their purpose, and how they relate to presentation controllers). The Popover controller is the only publicly available custom presentation controller in the UIKit. It gets special treatment in your view controller. You have a presentation controller property, but also a specific Popover presentation controller property. If you have a presentation controller, or a Popover, it will be returned in the presentation controller.

Private API (12:38)

When you set these modal styles, almost all of them map into this full screen presentation controller (smaller on iPad):

// Modal styles: .FullScreen, .FormSheet, .PageSheet, etc.
_UIFullscreenPresentationController

// For alerts, we have the UI alert controller
// alert presentation controller - Two alerts, two controllers
_UIAlertControllerAlertPresentationController

// On iPhone, it is presented with this ActionSheet compact
// presentation controller; on iPad, an action sheet displays
// as a Popover in certain scenarios
_UIAlertControllerActionSheetCompactPresentationController

// ActionSheet on iPad (Popover)
_UIAlertControllerActionSheetRegularPresentationController

API summary (13:58)

  • 4 methods to display a view controller
  • Set styles via an enum value
  • Use PopoverPresentationController directly
  • Provide a custom Presentation Controller

API is easy to misuse (e.g. set some properties that should not be set with different styles and enums), and the UIKit may start throwing exceptions. Read the documentation for good practices.

How to Use Custom Presentation Controllers (15:00)

  1. Subclass UIPresentationController
  2. Vend controller via transitioning API

1. Subclass: Transitioning and Chrome (15:21)

You sub-class UI presentation controller, override some methods, and vend that controller via the transitioning API. When you are subclassing, two methods (transitioning, presenting and dismissal):

func presentationTransitionWillBegin()

func dismissalTransitionWillBegin()

Subclass: Positioning (15:48)

You have these two methods for positioning (dealing with the size of the child / parent view):

func sizeForChildContentContainer(_, withParentContainerSize:) -> CGSize
func frameOfPresentedViewInContainerView() -> CGRect

Subclass: Adaptivity (15:48)

func adaptivePresentationStyle() -> UIModalPresentationStyle
func shouldPresentInFullscreen() -> Bool
func containerViewWillLayoutSubviews()

In containerViewWillLayoutSubviews, similar to viewWillLayoutSubviews, you will need to override this to do layout and to handle rotations elegantly. If you are using autolayout you may not have trouble. If you set up your constraints, you may not have to implement that method.

You can return .none for that style, or the other provided styles, like .Fullscreen, .Popover, etc., but it is very limited. You should be able to return any custom presentation controller, I think. You have this custom controller and you have to adapt to environment changes (e.g. size class changes or rotation), but the only way you can do that is through the built in styles. You cannot supply your own custom adaptive controller.

2. Vend Controller (17:24)

Vending this controller, you will conform to the UIViewControllerTransitioningDelegate API. The Presenting view controller conforms to this. You set a delegate on the Presented controller. If you are in your ViewController, it’ll look something like this:

let vc = MyViewController()
vc.transitioningDelegate = self

You will use this to see who is going where (from what controller to which controller), and return the correct presentation controller.

func presentationControllerForPresentedViewController(_,
    presentingViewController:,
    sourceViewController:) -> UIPresentationController?

Demo (18:30)

To get an idea of what we can do with a Swifty API, see the video for this section. I demo Push, Modal, Show/ShowDetail, Popover, PopoverFrontView, and Custom views.

Having this API is so much better than view controller containment. Before, you would have had to add a child view controller and manage the transitions yourself, adding the subviews and laying out those subviews in your view hierarchy. When you need this functionality, presentation controllers are generally a better option than view controller containment.

Let’s Build a Micro-Library (21:00)

enum PresentationType {
    case Modal(NavigationStyle, UIModalPresentationStyle, UIModalTransitionStyle)
    case Popover(PopoverConfig)
    case Push
    case Show
    case ShowDetail(NavigationStyle)
    case Custom(UIViewControllerTransitioningDelegate)
}

For the NavigationStyle enum: None, or WithNavigation:

enum NavigationStyle {
    case None
    case WithNavigation
}

You have probably run into this scenario: you have a view controller, and you want to push it on the navigation stack. You get the chrome for free, you get the back button, and you do not need to embed that in a navigation controller because you are pushing it on an existing stack. If you want to present that modally, you still want nav bar in the title and the cancel button in a navigation controller, and then pop that up modally. Popovers can be displayed from two areas: bar button items or any View. UI BarButtonItem is not a UIView. If you are presenting a Popover, and you do not set these up correctly, you will get exceptions. This struct will have the Source, the ArrowDirection, and an optional delegate to set up the Popover.

struct PopoverConfig {
    enum Source {
        case BarButtonItem(UIBarButtonItem)
        case View(UIView)
    }

    let source: Source
    let arrowDirection: UIPopoverArrowDirection
    let delegate: UIPopoverPresentationControllerDelegate?
}

All will map into this new method (that we add to view our view controller). It is similar to methods where we present a view controller, except now we will give it the PresentationType. We have this single point of entry into this one method to call (see below). You are mapping this configuration data in that PresentationType enum back down to the UIKit properties and methods.

extension UIViewController {

// maps PresentationType to UIKit properties and methods
func presentViewController(
                     controller: UIViewController,
                     type: PresentationType,
                     animated: Bool = true)
}

Usage (24:44)

If we want to push a view controller:

presentViewController(vc, type: .Push)

If we want to use a custom Popover:

let config = PopoverConfig(source: .View(v))
presentViewController(vc, type: .Popover(config))

With modals:

let type = .Modal(.WithNavigation, .FullScreen, .CoverVertical)
presentViewController(vc, type: type)

With our custom type, it is very clear that we have this custom controller:

presentViewController(vc, type: .Custom(self))

self would be the view controller that you are in (it would call that one method that returns your custom controller). I would love to give the custom enum the actual controller itself, but with this API you cannot set the presentation controllers directly. In fact, they are read-only. When you return them from that transitioning method, that is the mechanism by which you can set those properties.

Swifty API (26:15)

More expressive, concise. You have a single entry point into the API: it is more structured, and less likely to misuse. Enums force us to set it up correctly.

Additional Resources (26:36)

Q&A (27:40)

Q: Do you have a good solution for handling changing size classes inside a custom presentation controllers; can you read whether it is regular or compact?

Jesse: There are some methods that you can implement (there is one that I missed on that slide), it will pass in the trait collection, and that would be a method that you override. There are delegate methods on Popover controller that might be on the super class. You can use them as hooks to deal with that adaptivity.

Q: How would you deal with interactive transitioning delegates?

Jesse: That is a separate thing. Part of the UIView transitioning delegate API is where you can return those animator objects in interactive animators. We only looked at one method, which returns the presentation controller. The other methods in that delegate protocol deal with the transitioning APIs - those would work. The presentation controller is managing this separate area they should play nicely together, they are not coupled in any way. One thing I did forget to mention, with that half modal presentation controller, we were using the built in transition style. As with the modal transition style enum, or the modal presentation style enum, you have a corresponding transition style: there is cover vertical, across dissolve, the one that flip horizontal (I think). UIKit will be vending custom animator objects. With our half modal, it presented with the cover vertical animation, coming up from the bottom. We could have easily done across dissolve and used the built in UIKit styles. You can mix these how you want.

Q: You mentioned the containment API’s. I use containment API’s in their iOS 5. Take a look at presentation controller, and that seemed too much API for some things. I was surprised to hear you say that is much better; is there any time where containment API is preferable? I feel the APIs are being put forward for this new presentation controller, then it is well to size class awareness, view controller with inside of a larger contextual view controller that you are going to go right back, but more statically presenting VC’s and making your own nav drawer bar.

Jesse: Right, that would be a perfect use case of using presentation controllers for the hamburger menu.

Q: Is there a point that you still would fall back to the containment APIs, or is this french going forward?

Jesse: Depending on your view; take the App Store app as an example. The banner at the top, you have different sections for different categories for the top apps. Always the horizontal scrolling sections. I would use view controller containment for each of those collection views that are there; they would each have their own child view controller (you do not have this massive view controller). The root controller would manage all the children and when you are on the same level, containment is the right way to go. But when you want to present extra or temporary views, before it was common to use view controller containment to add those temporarily and dismiss them if you have, for example, options to select, that is much better served through a presentation controller now. In this example we dimmed the background and prevented interaction. But you can customize that to interact with both controllers at the same time. You can set up delegates between them to pass data between the two controllers. The benefit is: Popover or presentation controllers are reusable. You can put any view controller into that presentation controller (vice versa). It helps you build up some of these reusable components throughout your app. On the other hand, contaiment can more tightly couple these different controllers and views.

Jesse Squires

Jesse Squires is software developer in San Francisco who’s passionate about mobile computing, open-source, and cortados. He is currently working on iOS at Instagram. His interests include software design patterns and architecture, cognitive science and philosophy, animated GIFs, and body modification.