Ad

Our DNA is written in Swift
Jump

An Update on Custom Modal Presentations

Customizing a modal presentation of a view controller would be tricky before iOS 7. It got much easier with the addition of the transitioningDelegate protocol. This delegate would be able to vend an animation controller and an interaction controller for presentation and dismissal on the view controller which implemented them.

Without that, if you wanted a “burger menu” then you had to implement a custom container controller like I did with DTSidePanel in DTFoundation 1.4, three years ago. Things have gotten much easier a year ago.

A client of ours liked the way the Linked In app showed related apps in a modal side panel and so I went to research how you would that most reusably nowadays.

I am providing a sample app in my Swift Examples repo on GitHub, named ModalSidePanelPresentation if you care to follow along.

modal side panel

I generally felt that there is duplicate functionality between custom UIStoryBoardSegues and using animation controllers. Where would you put the animation code? And what about supporting unwinding with custom modal presentations?

Both segues and delegates have another problem: both are quite short-lived. The transitioning delegate – like all delegates should be – is defined as weak. A segue is only created for a transition and is deallocated as soon as the transition is over.

Presentation Transitions

To clarify terms: when you present a view controller A from a view controller B, you have a transition animation from A to B. While B is visible and the user interacts with it you call B the presentedViewController and A is the presentingViewController. In short: the presentation is ongoing. It all ends when the user triggers a dismissal of B, either by an unwind seque defined in the storyboard or by calling dismissViewController. The subsequent transition animation leads us back from B to A.

Present → Transition → Presentation → Dismiss → Transition

As of iOS 7, there was something missing to model something that would control the state of the presentation going on between the two transitions. So, in iOS 8, Apple introduced UIPresentationController with UIPopoverPresentationController being a concrete example. The transitioning delegate also got a new delegate method to provide a presentation controller for a view controller.

We had gotten the ideal object for keeping track of the elements of the presentation and dismissal animations. Note though, that there was no way to specify transitioning delegates or presentation controllers from storyboards.

You don’t want to have to add presentation-related code to each view controller which you plan to be presenting a certain modal way. This would unnecessarily duplicate code as well as make it difficult to reuse view controllers with other presentations.

But, there is a way …

Using Segues for Custom Presentations

For a recent client project I worked out the following: we’ll have a custom modal presentation controller which is is also a transitioning delegate vending itself for both transitions. Then we’ll have a custom segue which creates such a presentation controller and sets the transitioning delegate for the presented view controller.

import UIKit

// private variable that sticks around even after the segue is has been released
internal var _modalPresentationController: ModalSidePanelPresentationController!

/// A custom segue that uses the ModalSidePanelPresentationController for presenting a 
/// modal view controller
public class ModalSidePanelPresentationSegue: UIStoryboardSegue
{
   /// Designated initializer
   override init(identifier: String?, source: UIViewController, 
                 destination: UIViewController)
   {
      super.init(identifier: identifier, source: source, 
                 destination: destination)
        
      _modalPresentationController = ModalSidePanelPresentationController(
         presentedViewController: destination, 
         presentingViewController: source)
   }
    
   /// Executes the presentation
   public override func perform()
   {
      destinationViewController.transitioningDelegate = _modalPresentationController
      sourceViewController.presentViewController(
         destinationViewController, animated: true, completion: nil)
   }
}

We overwrote the designated initializer of the segue to create our modal side panel presentation controller and store it in an internal global variable. The perform function simply sets the transitioning delegate and calls the usual function for presenting a view controller.

A Custom Presentation Controller

There is one more detail necessary to complete the setup: the presentation style of the presented view controller needs to be set to .Custom. We’ll do that in the presentation controller’s init. This way we don’t have to change it for all thusly presented view controllers in the story board.

class ModalSidePanelPresentationController: UIPresentationController
{
   override init(presentedViewController: UIViewController, 
                 presentingViewController: UIViewController) {
      super.init(presentedViewController: presentedViewController,
                 presentingViewController: presentingViewController)
        
      // style needs to be custom so that the presentationController transitioning
      // delegate method is being called
      presentedViewController.modalPresentationStyle = .Custom;
   }

The init of our presentation controller passes on the presented as well as the presenting view controller and conveniently stores them in two instance variables.

The implementation of the transitioning delegate is quite boring, please don’t be mad that I am still showing it here. Note though that in true Swift style we do it in an extension to the presentation controller class.

extension ModalSidePanelPresentationController: UIViewControllerTransitioningDelegate
{
   func animationControllerForPresentedController(presented: UIViewController,
      presentingController presenting: UIViewController, 
      sourceController source: UIViewController) 
      -> UIViewControllerAnimatedTransitioning?
   {
      return self
   }
    
   func animationControllerForDismissedController(dismissed: UIViewController)
      -> UIViewControllerAnimatedTransitioning?
   {
      return self
   }

    
   func presentationControllerForPresentedViewController(
      presented: UIViewController, 
      presentingViewController presenting: UIViewController, 
      sourceViewController source: UIViewController) 
      -> UIPresentationController? 
   {
      return self
   }
}

We will implement two methods of the transitioning delegate, one to specify the animation duration, one to carry out the animation. But to know which direction we are animating in (presenting/forward or dismissing/backward) we can use the presentation controller methods presentationTransitionWillBegin and dismissalTransitionWillBegin. This is the ideal place to story the current direction in an instance variable.

Look at these methods in the sample app to see this working. There you’ll also see the actual animation code which I split into separate functions for the forward and backwards animation.

extension ModalSidePanelPresentationController: UIViewControllerAnimatedTransitioning
{
   func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval
   {
      return 0.50
   }
    
   func animateTransition(transitionContext: UIViewControllerContextTransitioning)
   {
      if isPresenting
      {
         animatePresentingTransition(transitionContext)
      }
      else
      {
         animateDismissingTransition(transitionContext)
      }
   }

   // see sample for actual animation functions 
}

I found that this splitting of the animation functions made the code much simpler. In the presentation controller methods we also calculate a frame for the presented view in frameOfPresentedViewInContainerView. This is another feature of presentation controllers: you can do a non-full-screen presentation by specifying a frame. The default is to use the transition container’s bounds.

Conclusion

One more thing I only realized while writing this post was that originally I had a retain cycle. But this was broken by nilling the global variable after the dismissal transition was over.

The sample app demonstrates adding a black view underneath both view controllers to give the effect of stepping outside of the app. And a tap handler lets you dismiss the modal presentation. Oh and round corners during the presentation as well. Not to mention that the second view controller has a light status bar style…

The resulting combo of these two objects, a segue and a presentation controller makes it super simple to add this special effect to any modal segue. All you need to do is replace the segue class with this one.

 


Also published on Medium.


Categories: Recipes

Leave a Comment