Make App Pie

Training for Developers and Artists

The Step by Step Guide to Custom Presentation Controllers

Ever wanted that sliding sidebar or an alert with a image picker? Apple has many great ways of presenting view controllers,  but sometimes we want something different, something new. For that we subclass UIPresentationController. However there’s a few concepts that you’ll need to wrap your head around first. In this lesson we’ll create a few standard controllers to explain the custom controllers, then make and animate our own custom controller sliding  side bar. We’ll build the presentation controller step by step, so you know how it really works.

 Custom Presentation Anatomy in Alert Views.

Make a new project called SwiftCustomPresentation, with a universal device and Swift as the language.  Once loaded go to the launchscreen.storyboard. I like to have some indication that things are working, and since Apple removed the default launch screen layout, I tend to add my own. Set the background color to Yellow(#FFFF00). Drag a label on the launch screen  and title it Hello in 26 point size.  Click the alignment auto layout buttonalignment icon at the bottom of the storyboard and check on Horizontally in Container and Vertically in Container set to 0. Update the frames for  Items  of  new constraints, then click Add 2 constraints.

2016-04-07_06-09-52

You now have a nice label in the center of the launch screen

2016-04-07_06-14-05

Go to the storyboard. Make the background Yellow(#FFFF00). Drag a button to the center of the storyboard. Title the label Hello, Pizza as a 26 point size system font. Make the text color Dark Blue(#0000AA) Just like the label on the launch screen, align the button to the center of the storyboard.  Click the alignment auto layout buttonalignment icon at the bottom of the storyboard and check on Horizontally in Container and Vertically in Container set to 0. Update the frames for  Items  of  new constraints, then click Add 2 constraints.

2016-04-07_06-36-27

Open the assistant editor. Control drag from the button to the code. Create an action entitled showAlert. Close the assistant editor for now. Ooen the ViewController.swift file.  Change the action to this:

@IBAction func showAlert(sender: UIButton){
helloAlert()
}

Now add the following code to create an alert

func helloAlert(){
     let alert = UIAlertController(
          title: "Hello Slice",
          message: "Ready for your slice?",
          preferredStyle: .Alert)
      let action = UIAlertAction(
          title: "Okay",
          style: .Default,
          handler: nil)
      alert.addAction(action)
      presentViewController(alert,
          animated: true,
          completion: nil)
}

This is  code to present the simplest alert possible.  We create a simple alert and give it one action, an Okay button that dismisses the alert. The last line is the important one for our discussion:


presentViewController(alert,
     animated: true,
     completion: nil)

As of iOS8, all modal view controllers present with this one method.  You tell the method which controller you want to present, set a few properties on it and it does the work, no matter what your device. It relieves a lot of the hard work of developers specifying exceptions for individual devices.

Set your simulator for an iPhone 6s. Build and run.  When the Hello pizza button shows in the simulator, tap it. You get your alert.

2016-04-07_07-01-03

We’ve made this alert to illustrate the three parts you need to know about any custom view controller.

2016-04-07_07-01-10

Our yellow background is the presenting view controller, the controller that calls the presentViewController method The alert, which is the presented view controller, is the controller presentViewController uses in its parameters.

presentingViewController.presentViewController(presentedViewController, animated:true,completion:nil)

In an alert and in other view controllers such as popovers, the presentViewController method adds a special view  between the presented and presenting view controllers called a chrome. The chrome is the difference or padding between the bounds of the presented controller, and the bounds of the presenting controller. Usually it is a solid color with some alpha value. Apple tends to use a gray color for chrome, which is why yellow is looking like a dark orange yellow.

Adding a Modal View

Alerts help us explain the anatomy of a presentation controller. However they are rather automatic in their execution. Let’s add a simple modal view controller to play with custom view controllers.  Press Command-N and select a new Cocoa Touch Class ModalViewController. Do check on Also Create XIB file. This is one of those places I prefer xibs due to portability over the storyboard.

2016-04-07_08-16-39

Save the file. Go to  the ModalViewController.xib file. Change the background to Dark Blue(#0000AA). Add two labels to the xib. In one make the text Ready for your slice? , a Font of 26.0 Bold, and White(#FFFFFF) text. Set the Alignment to Left justified. Set the  Lines  property to 0 and the Line Breaks  to Word Wrap. This will let the label vary the number of lines to fit the text, and word wrap the results. Your attributes should look like this:

2016-04-07_08-32-54

For the second label, add a short paragraph. I used a passage from Episode 4 of my podcast A Slice of App Pie.

This is the other thing about persistence: It does not see success as an absolute. It is not a Bool, a true/false variable, but a Double,  a real number. It is not just black or white, but every color.  You are not a success, but you are at a degree of success. Your past experience fuels your future success.

Set the  Font to 16.0 Regular, and White(#FFFFFF) text. Set the Alignment to Full  Justified. Set the  Lines  property to 0 and the Line Breaks  to Word Wrap.

Select the text paragraph and Control drag  down and to the right until you are over the blue of the background.

2016-04-07_08-59-30

Release the mouse button and you will see an auto layout menu. Hold down shift and Select  Trailing Space to Container, Bottom Space to Container and Equal Widths:

2016-04-07_08-46-29

 Click the Add Constraints button. Now control-drag from the Ready label to the paragraph label.  In the auto layout menu that appears, Shift-Select Vertical Spacing, Trailing, and Equal Widths

2016-04-07_08-53-44

 Click the Add Constraints button. Select the paragraph and go to  the size inspector 2016-04-07_08-53-46.  You’ll see a list of constraints:

2016-04-07_08-55-49

Your values will most likely be different than the illustration. Click the edit button for the Trailing Space to: Superview. A popup appears. Change the Constant to 8.

2016-04-07_08-56-11

Click the Equal Width: to Superview edit button. Change the Multiplier to 0.9.

2016-04-07_08-56-32

Edit the Bottom Space to: Superview constraint. Change the Constant to 20.

Edit the Align Trailing Space to: Ready for yo… constraint. Change the Constant to 0.

Edit the Top Space to: Ready for yo… constraint. Change the Constant to 20.

Edit the Equal Width to: Ready for yo… constraint. Change the Multiplier to 0.5.

If you’ve never worked with auto Layout before, what we just did is anchor the bottom right corner of the paragraph to the bottom right corner of the xib. with a margin of 20 points on the bottom and 8 points on the right. We set the width of the paragraph to 90% of the width of the xib. The Label above it, acting as a title, we made half as long as the paragraph, and aligned to the right side of the paragraph, 20 points up.

Click  the triangle tie fighter 2016-04-07_09-05-05 which is the auto layout  resolve button. You get a menu like this:

2016-04-07_09-04-46

Select for the All Views in View section Update Frames. You now have a layout like this:

2016-04-07_09-04-47

We’ll dismiss this modal with a swipe to the right. In the object library,  find the swipe gesture:

2016-04-07_10-00-48

Drag it to the blue of the xib and release. Nothing will happen there but in the document outline (if you don’t have it open click the  2016-04-07_10-07-37button)  you will see a new object listed

2016-04-07_10-10-08

Select the Swipe Gesture Recognizer and in the attributes menu, you should see the following:

2016-04-07_10-11-47

These are the defaults and what we want: a single finger swipe to the right.  Open the assistant editor and control-drag the Swipe Gesture Recognizer from the document outline to the code. Create an action named dismissModal.

2016-04-07_10-16-20

In the code that appears, dismiss the vew controller:

@IBAction func dismissModal(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
}

Presenting the View Controller.

Go back to the ViewController.swift File.  Add the following method:

 func presentModal() {
let helloVC = ModalViewController(
    nibName: "ModalViewController",
    bundle: nil)
    helloVC.modalTransitionStyle = .CoverVertical
presentViewController(helloVC,
    animated: true,
    completion: nil)
}

The code gets the view controller  and the xib then presents it. We’re using the default settings for a modal in this case.

Go to the storyboard.  Open the assistant editor if not already open. Drag a Swipe Gesture recognizer to the storyboard. Select it in the the document outline and change the Swipe attribute  to Up. Control-drag from the Swipe Gesture recognizer and make an outlet

@IBAction func swipeUp(
    sender: UISwipeGestureRecognizer) {
    presentModal()
}

Build and run.You should be able to swipe up and show the blue controller.

2016-04-07_10-43-02

The title is too small to fit on one line and adapts to two. Rotate the phone (in the simulator press Command right-arrow) and you get a slightly different layout:

2016-04-07_10-43-26

Swipe right and the view dismisses

2016-04-07_13-43-29

Custom Presentation :
A  Big Alert View

We’ve got a working modal view controller. We did this for several reasons. This proves there’s no magic in what we are about to do. It also reinforces how we do this in a standard presentation controller.

Adding the Custom Presentation Controller

Create a new class by pressing Command-N on the key board in Xcode. Make a new cocoa touch class MyCustomPresentationController subclassing UIPresentationController.  You end up with an empty class:

class MyCustomPresentationController: UIPresentationController {
}

Our first task is to add the chrome.  We’ll also add a constant for the chrome color.  Add this to the class

let chrome = UIView()
let chromeColor = UIColor(
     red:0.0,
     green:0.0,
     blue:0.8,
     alpha: 0.4)

A presentation controller needs to do three things: Start the presentation, end the presentation and size the presentation. The UIPresentationController class has three methods we override to do this: presentationTransitionWillBegin, dismissalTransitionWillBegin and frameOfPresentedViewInContainerView. There is a fourth method containerViewWillLayoutSubviews which may sound familiar to those who have worked with code and auto layout. When there is a change to the layout, usually rotation, this last method will make sure we resize everything.

First we present the controller. In our code we control what the chrome does. To our custom presentation controller, add the following method

override func presentationTransitionWillBegin() {
    chrome.frame = containerView!.bounds
    chrome.alpha = 1.0
    chrome.backgroundColor = chromeColor
    containerView!.insertSubview(chrome, atIndex: 0)
}

First we set the size of the chrome by the size of containerView. The containerView property is a view which is an ancestor of the presenting view in the view hierarchy. containerView gives us a view that is bigger to or the same size as the presenting view. Setting the chrome to this size means the chrome will cover everything. We then set the color and alpha of chrome, and then add chrome to the container view.

Our next method to implement is dismissalTransitionWillBegin. We’ll remove the chrome from the view hierarchy here. Add this code:

 override func dismissalTransitionWillBegin() {
    self.chrome.removeFromSuperview()
 }   

We’ll change the size of the presented view so we can see the chrome. Size changes take two methods. The first is frameOfPresentedViewInContainerView. Add the following code:

override func frameOfPresentedViewInContainerView() -> CGRect {
    return containerView!.bounds.insetBy(dx: 30, dy: 30)
}

This returns the frame of the container. We used the insetBy function to shrink it 30 points within the container view. This will give us a effect similar to an alert.

To make sure adaptive layout changes everything, we need one more method. add this:

override func containerViewWillLayoutSubviews() {
    chrome.frame = containerView!.bounds
    presentedView()!.frame = frameOfPresentedViewInContainerView()
}

Our first line changes the chrome’s size to fit the new size of the view. The second one makes sure that the presented view is the correct size by calling the method we just wrote frameOfPresentedViewInContainerView.

Adding the Delegate

Custom presentation controllers run through a delegate. Above the MyCustomPresentationController class, add the following:

class MyTransitioningDelegate : NSObject, UIViewControllerTransitioningDelegate {
    func presentationControllerForPresentedViewController(
        presented: UIViewController,
        presentingViewController presenting: UIViewController,
        sourceViewController source: UIViewController
    ) -> UIPresentationController? {
        return MyCustomPresentationController(
            presentedViewController: presented,
            presentingViewController: presenting)
    }
}

This is a subclass of the UIViewControllerTransitioningDelegate. Though the parameters are long, the single function in the delegate returns our presentation controller, using the designated initializer for the class.

That is all we need for a bare bones custom controller. Our next step is to use it.

Using the Custom Presentation Controller

We’ll add a left swipe gesture for presentation this controller. Go to the storyboard. Drag a swipe gesture control on the view. Set the Swipe attribute this time for Left. Open the assistant editor and control-drag this gesture to the ViewController class, making a new action swipeLeft. Change the new function to this:

@IBAction func swipeLeft(sender: UISwipeGestureRecognizer) {
    presentCustomModal()
    }

Close the assistant editor and go to the ViewController.swift code. Add the following to the ViewController class, under to the presentModal function so you can compare the two

func presentCustomModal(){
    let helloVC = ModalViewController(
        nibName: "ModalViewController",
        bundle: nil)
    helloVC.transitioningDelegate = myTransitioningDelegate
    helloVC.modalPresentationStyle = .Custom
    helloVC.modalTransitionStyle = .CrossDissolve
    presentViewController(helloVC,
        animated: true,
        completion: nil)
    }

Like the standard modal controller, we get the controller and present it. We did change the transition style to tell the difference between the two controllers, and as you’ll see a cross dissolve makes a better transition for this type of view.

Unlike presentModal, we set the modalPresentationStyle to .Custom so presentViewController knows to look for a custom presentation controller. However it won’t work without setting the view controller’s transitioningDelegate property. The transitioning delegate tells the view controller where the custom transition is. If this is nil, the presentation is a standard presentation, not a custom one.

We haven’t set the delegate yet. At the top of the ViewController class, add this line:

let  myTransitioningDelegate = MyTransitioningDelegate()

Build and Run. Once Hello,Pizza appears, swipe left and the modal view appears with chrome around it.

2016-04-08_07-14-47

Rotate the device .

2016-04-08_07-15-00

Swipe right and the view disappears

Why Apple Uses Gray Chrome

You’ll notice that the chrome does not turn blue, but instead tuns green. As a transparent color, it blends with the color under it, sometimes not in the most attractive way. This is why Apple uses gray for chrome — you never have the problem with gray. If we change the chrome’s backgroundColor like this:

//let chromeColor = UIColor(red:0.0,green:0.0,blue:0.8,alpha: 0.4) //mixes with background colors
let chromeColor = UIColor(white: 0.5, alpha: 0.6) //gray dims the background

Then build and run. We get a  slightly different effect:

2016-04-08_07-20-12

You’ll notice in both these cases however, the presented controller seems to glow. This is not because of code, but a trick of your brain called Simultaneous contrast . The brain has a hard time dealing with two hues that are complements next to each other, so it starts to make things up to compensate. I used two colors most likely to do this to each other, blue and yellow. There are two strategy you can use to prevent this. One is use less jarring colors in your color scheme for your app. The second is to change the chrome to have a high alpha value and not be as transparent. Change the code to this:

//let chromeColor = UIColor(red:0.0,green:0.0,blue:0.8,alpha: 0.4) //mixes with background colors
    //let chromeColor = UIColor(white: 0.4, alpha: 0.6) //gray dims the background
    let chromeColor = UIColor(red:0.0,green:0.0,blue:0.8,alpha: 0.7) // not as transparent

The chrome is very blue, and the effect is far less.

2016-04-08_07-13-34

Set the chrome back to the gray for the rest of the tutorial.

Animating the Chrome

You’ll notice that the chrome appears and disappears rather abruptly. We need to add some animation to the chrome so it transitions smoothly. There is  an object called a transition coordinator that automatically loads into the view controller during presentation. It gives you a way to do animation parallel to the main animation.  There’s a method in the transition coordinator animateAlongTransition:completion: that we’ll use to fade the chrome as the presented controller fades in.

In the MyCustomPresentationController class, change the method to this

override func presentationTransitionWillBegin() {
    chrome.frame = containerView!.bounds
    chrome.alpha = 0.0  //start with a invisible chrome
    //chrome.alpha = 1.0
    chrome.backgroundColor = chromeColor
    containerView!.insertSubview(chrome, atIndex: 0)
    presentedViewController.transitionCoordinator()!.animateAlongsideTransition(
        {context in
            self.chrome.alpha = 1.0
         },
         completion: nil)
}

We changed to code to start the chrome with an alpha of 0. The transitionCoordinator function returns the transition coordinator as an optional value. We use its animateAlongsideTransition method to transition for the invisible chrome to a visible one.

In the presentationTransitionWillBegin, we do nothing in the completion closure of animateAlongsideTransition. On the other hand, we’ll remove the chrome in the dismissalTransitionWillBegin. Add this code.

override func dismissalTransitionWillBegin() {
        presentedViewController.transitionCoordinator()!.animateAlongsideTransition(
            { context in
                self.chrome.alpha = 0.0
            },
            completion: {context in
                self.chrome.removeFromSuperview()
            }
        )
        //self.chrome.alpha = 0.0
        //self.chrome.removeFromSuperview()
    }

We fade out the chrome, and once the chrome completely fades at the end of the main animation, we remove the view.

With those changes, build and run. The chrome fades in and out smoothly

Custom Presentation #2:
A Sidebar

A comma use for a custom presentation controller is a toolbar or side bar. Let’s modify the code to make a sidebar on the right side of our device.

Changing the frame

For a side bar we are setting the view on one side of the contentView instead of the center as we do with insetBy. Change frameOfPresentedViewInContainerView to

override func frameOfPresentedViewInContainerView() -> CGRect {
        //return containerView!.bounds.insetBy(dx: 30, dy: 30)  //center the presented view
        let newBounds = containerView!.bounds
        let newWidth:CGFloat = newBounds.width / 3.0 // 1/3 width of view for bar
        let newXOrigin = newBounds.width - newWidth //set the origin from the right side
        return CGRect(x: newXOrigin, y: newBounds.origin.y, width: newWidth, height: newBounds.height)
    }

We commented out the insetBy, and took control of the frame directly. We take the size of the container view, and divide by three to make a frame size  a third the width of the container. We also set the origin to one-third less the width of the container view to make the bar on the right. Build and run. It’s there, but portrait does not look so good.

2016-04-08_07-57-11

Landscape looks a lot better, since it has more space

2016-04-08_07-57-25

We can change the proportions to fit better. We’ll make it 3/4 of the width. Change the code to this:

var newWidth:CGFloat = newBounds.width * 0.75 // 3/4 width of view for bar

This will work, but will be way too big on an iPad or iPhone 6 plus in landscape. We can use a trait collection to reduce the size to one third on the bigger devices. add this to the code, just under the newWidth declaration:

if containerView!.traitCollection.horizontalSizeClass == .Regular{
            newWidth = newBounds.width / 3.0 //1/3 view on regular width
        }

Change your simulator to an iPhone 6s plus. Build and run. In  portrait, we get a 3/4 view

2016-04-08_08-16-42

In landscape, with the regular width size class, we get a  1/3.

2016-04-08_08-17-04

Change to an iPad Air 2 simulator. We get 1/3 on both landscape and portrait.

2016-04-09_05-54-21 2016-04-09_05-53-58

 Animating the Side Bar

Side bars look best when they transition by sliding in. While we can animate the chrome with animateAlonsideTransition, we cannot do so for the frame of the presented controller. To do that we need to use another class,  UIViewControllerAnimatedTransitioning.

While making a new class is probably a better way of doing this, I’m going to keep it  together with the presentation controller.  Just under  the import UIKit in MyCustomPresentationController.swift, add the following:

class MyAnimationController: NSObject,UIViewControllerAnimatedTransitioning{
    var isPresenting :Bool
    let duration :NSTimeInterval = 0.5
}

The  UIViewControllerAnimatedTransitioning  protocol contains a series of functions to create an animation for both dismissal and presentation.  It has two required functions, where we use these two properties.

We added two properties to our class. isPresenting will be our indicator if this is a dismissal or presentation. To make it easier to use, add this to the class:

init(isPresenting: Bool) {
    self.isPresenting = isPresenting
    super.init()
}

This will set the value when we initialize the class. We’ll use it in a required method animateTransition. This method tells the system what to do when there is a transition. We’ll set it up to do both a presentation animation and a dismissal. Add this code:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    if isPresenting {
        animatePresentationTransition(transitionContext)
    } else {
        animateDismissalTransition(transitionContext)
    }
}

The parameter transitionContext type UIViewControllerContextTransitioning contains all the information you need to do the animation transition, including the presented and presenting controllers and the container view. We’ll pass those on to two more functions we’ll write shortly to animate presentation and dismissal of the presenting view controller.

Our other property, duration will set the time for the animation duration in the other required function. Add this code:

 func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return duration
    }

This satisfies the two required functions for the protocol. We are still getting errors for out two animation functions though. Add these to the class:

func animatePresentationTransition(transitionContext: UIViewControllerContextTransitioning){
}
func animateDismissalTransition(transitionContext: UIViewControllerContextTransitioning){
}

Adding a presentation Animation

Our first step is to add a presentation animation in animatePresentationTransition. We animate using one of the UIView animation class methods animateWithDuration. There are different version for different effects, whihc you can read more about in the UIView Class Refrence. All of these will use closures to describe the end state of the animation. We set the presenting view controller to a state where we will star the animation, then in the animation block give its final state. The animateWithDuration does the rest.

To present the controller’s animation, we’ll need a few thing from the the transition context. Add this to the code for animatePresentationTransition

        // Get everything you need
        let presentingViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
        let finalFrameForVC = transitionContext.finalFrameForViewController(presentingViewController)
        let containerView = transitionContext.containerView()
        let bounds = UIScreen.mainScreen().bounds

We get the presetingviewController to be able to set its state before and after animation. We also get the container view for adding the presenting view controller. The finalFrameForVC is the CGFrame that will be the end of the animation. We’ll use bounds to give us the bounds of the visible screen.

The next step is to change the frame of the presenting view controller to where it should be before the animation begins. we want it off to the right side of the visible view. Since we are not moving anything in the Y direction, we want to have our starting point as the width of the visible view. Add this code:

        //move our presenting controller to the correct place, and add it to the view
presentingViewController.view.frame = CGRectOffset(finalFrameForVC, bounds.size.width, 0)
presentingViewController.view.alpha = 0.0
containerView!.addSubview(presentingViewController.view)

Besides the presenting view moving off of stage right, I also dimmed the view to an alpha of 0.0. I then added the view to the container view. The next step is the big one — add the animation with this code:

//animate the transition
UIView.animateWithDuration(
    transitionDuration(transitionContext), //set above
    delay: 0.0,
    options: .CurveEaseOut, //Animation options
    animations: { //code for animation in closure
        presentingViewController.view.frame = finalFrameForVC
        presentingViewController.view.alpha = 1.0
    },
    completion: { finished in  //completion handler closure
        transitionContext.completeTransition(true)
    }
)

There’s a lot of unpack in this method’s parameters. We start with a duration we get from the transitionDuration method we defined earlier. Next thre is a delay to begin the animation, which we leave at ). The next parameter options takes a value from UIViewAnimationOptions to describe the behavior of the animation.

The next parameter, animations, describes the final state of the animation. In our code it sets the view controller’s frame to the final frame fro the presentation, and sets the alpha to 1.

Once the animation is complete there is a completion handler, where we set in the transition context a flag that says we are done animating.

For a dismissal, we do the same in reverse.Add this to animateDismissalTransition:

    
let presentedControllerView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let containerView = transitionContext.containerView()

We first get our presented controller view and the container view. This time, the current position of the frame is the position we start from, so there is no setting the initial frame of the animation. Instead we got straight into the animation, which animates the presented controller view to the edge of the container view. Add the animation:

        // Animate the presented view off the side
UIView.animateWithDuration(
    transitionDuration(transitionContext),
    delay: 0.0,
    options: .CurveEaseIn,
    animations: {
         presentedControllerView!.frame.origin.x += containerView!.frame.width
         presentedControllerView!.alpha = 0.0
    },
    completion: {(completed: Bool) -> Void in
         transitionContext.completeTransition(completed)
    }
)

Add the Animations to the Delegate

The UIViewControllerTransitioningDelegate does more than just hold the custom transition controller. It has optional methods for animation. Add to MyTransitioningDelegate the following

func animationControllerForPresentedController(
    presented: UIViewController,
    presentingController presenting: UIViewController,
    sourceController source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
        return MyAnimationController(isPresenting:true)
    }

After a long list of parameters, we return a MyAnimationController with isPresenting set to true. We set to false for a dismissal in this function

func animationControllerForDismissedController(
    dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
        return MyAnimationController(isPresenting:false)
}

Build and run. We now have an animated custom transition

Untitled

More Things to Try.

This rather long tutorial just breaks the ice in what you can do with a custom presentation. You do not have to use a xib, for example, Storyboards works just as well, with either segues or storyboard id’s plus a reference to the delegate. There are many more animation options as well.

Also a word of caution. I intentionally used a lot of optional values as explicit values to keep things simple and readable. You might not want to be as careless as I am here, and check for nil or optional chain much more often than I did in this code.

The Whole Code

ViewController.swift

//
//  ViewController.swift
//  SwiftCustomPresentation
//
//  Created by Steven Lipton on 4/7/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
    let  myTransitioningDelegate = MyTransitioningDelegate()
    
    //MARK: Actions
    @IBAction func showAlert(sender: UIButton){
        helloAlert()
    }
    @IBAction func swipeLeft(sender: UISwipeGestureRecognizer) {
        presentCustomModal()
    }
    
    @IBAction func swipeUp(sender: UISwipeGestureRecognizer) {
        presentModal()
    }
    //MARK: Instance methods
    func helloAlert(){
        let alert = UIAlertController(
            title: "Hello Slice",
            message: "Ready for your slice?",
            preferredStyle: .Alert)
        let action = UIAlertAction(
            title: "Okay",
            style: .Default,
            handler: nil)
        alert.addAction(action)
        presentViewController(alert,
            animated: true,
            completion: nil)
    }
    
    func presentModal() {
        let helloVC = ModalViewController(nibName: "ModalViewController", bundle: nil)
        helloVC.modalTransitionStyle = .CoverVertical
        presentViewController(helloVC, animated: true, completion: nil)
    }
    
    func presentCustomModal(){
        let helloVC = ModalViewController(nibName: "ModalViewController", bundle: nil)
        helloVC.transitioningDelegate = myTransitioningDelegate
        helloVC.modalPresentationStyle = .Custom
        helloVC.modalTransitionStyle = .CrossDissolve
        presentViewController(helloVC, animated: true, completion: nil)
    }
    
    //MARK: Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}


MyCustomPresentationController.swift

//
//  MyCustomPresentationController.swift
//  SwiftCustomPresentation
//
//  Created by Steven Lipton on 4/7/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

class MyAnimationController: NSObject,UIViewControllerAnimatedTransitioning{
    
    var isPresenting :Bool
    let duration :NSTimeInterval = 0.75
    
    init(isPresenting: Bool) {
        self.isPresenting = isPresenting
        super.init()
    }
    
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return duration
    }
    
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        if isPresenting {
            animatePresentationTransition(transitionContext)
        } else {
            animateDismissalTransition(transitionContext)
        }
    }
    
    func animatePresentationTransition(transitionContext: UIViewControllerContextTransitioning){
        // Get everything you need
        let presentingViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
        let finalFrameForVC = transitionContext.finalFrameForViewController(presentingViewController)
        let containerView = transitionContext.containerView()
        let bounds = UIScreen.mainScreen().bounds
        
        //move our presenting controller to the correct place, and add it to the view
        presentingViewController.view.frame = CGRectOffset(finalFrameForVC, bounds.size.width, 0)
        presentingViewController.view.alpha = 0.0
        containerView!.addSubview(presentingViewController.view)
        
        //animate the transition
        UIView.animateWithDuration(
            transitionDuration(transitionContext), //set above
            delay: 0.0,
            options: .CurveEaseOut, //Animation options
            animations: { //code for animation in closure
                presentingViewController.view.frame = finalFrameForVC
                presentingViewController.view.alpha = 1.0
            },
            completion: { finished in  //completion handler closure
                transitionContext.completeTransition(true)
            }
        )
    }
    
    func animateDismissalTransition(transitionContext: UIViewControllerContextTransitioning){
        
        let presentedControllerView = transitionContext.viewForKey(UITransitionContextFromViewKey)
        let containerView = transitionContext.containerView()
        

        // Animate the presented view off the side
        UIView.animateWithDuration(
            transitionDuration(transitionContext),
            delay: 0.0,
            options: .CurveEaseInOut,
            animations: {
                presentedControllerView!.frame.origin.x += containerView!.frame.width
                presentedControllerView!.alpha = 0.0
            },
            completion: {(completed: Bool) -> Void in
                transitionContext.completeTransition(completed)
            }
        )
    }
}

class MyTransitioningDelegate : NSObject, UIViewControllerTransitioningDelegate {
    func presentationControllerForPresentedViewController(
        presented: UIViewController,
        presentingViewController presenting: UIViewController,
        sourceViewController source: UIViewController
    ) -> UIPresentationController? {
        return MyCustomPresentationController(
            presentedViewController: presented,
            presentingViewController: presenting)
    }
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return MyAnimationController(isPresenting:true)
    }
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return MyAnimationController(isPresenting:false)
    }
}

class MyCustomPresentationController: UIPresentationController {
    let chrome = UIView()
    //let chromeColor = UIColor(red:0.0,green:0.0,blue:0.8,alpha: 0.4) //mixes with background colors
    let chromeColor = UIColor(white: 0.4, alpha: 0.6) //gray dims the background
    //let chromeColor = UIColor(red:0.0,green:0.0,blue:0.8,alpha: 0.7) // not as transparent
    
    override func presentationTransitionWillBegin() {
        chrome.frame = containerView!.bounds
        chrome.alpha = 0.0
        //chrome.alpha = 1.0
        chrome.backgroundColor = chromeColor
        containerView!.insertSubview(chrome, atIndex: 0)
        var newframe = frameOfPresentedViewInContainerView()
        newframe.origin.x = newframe.width
        presentedViewController.view.frame = newframe
        presentedViewController.transitionCoordinator()!.animateAlongsideTransition(
            { context in
                self.chrome.alpha = 1.0
                self.presentedViewController.view.frame = self.frameOfPresentedViewInContainerView()
            },
            completion: nil)
    }
    

    override func dismissalTransitionWillBegin() {
        presentedViewController.transitionCoordinator()!.animateAlongsideTransition(
            { context in
                
                self.chrome.alpha = 0.0
            },
            completion: {context in
                self.chrome.removeFromSuperview()
            }
        )
        //self.chrome.alpha = 0.0
        //self.chrome.removeFromSuperview()
    }
    
    override func frameOfPresentedViewInContainerView() -> CGRect {
        //return containerView!.bounds.insetBy(dx: 30, dy: 30)
        let newBounds = containerView!.bounds
        var newWidth:CGFloat = newBounds.width * 0.75 // 3/4 width of view for bar
        //var newWidth:CGFloat = newBounds.width / 3.0 // 1/3 width of view for bar
        if containerView!.traitCollection.horizontalSizeClass == .Regular{
            newWidth = newBounds.width / 3.0 //1/3 view on regular width
        }
        let newXOrigin = newBounds.width - newWidth //set the origin 1/3 from the right side
        return CGRect(x: newXOrigin, y: newBounds.origin.y, width: newWidth, height: newBounds.height)
    }
    
    
    /*
     if containerView!.traitCollection.horizontalSizeClass == .Regular {
     newWidth = newBounds.width * 0.33
     } else { //compact and unknown 80%
     newWidth = newBounds.width * 0.8
     }
     */
    override func containerViewWillLayoutSubviews() {
        chrome.frame = containerView!.bounds
        presentedView()!.frame = frameOfPresentedViewInContainerView()
    }
}

ModalViewController.swift

//
//  ModalViewController.swift
//  SwiftCustomPresentation
//
//  Created by Steven Lipton on 4/7/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

class ModalViewController: UIViewController {

    @IBAction func dismissModal(sender: AnyObject) {
        dismissViewControllerAnimated(true, completion: nil)
    }
    
}

9 responses to “The Step by Step Guide to Custom Presentation Controllers”

  1. Anselme Kotchap Avatar
    Anselme Kotchap

    Hi Steven, thank you for doing your thing again and turn this rather confusing API (to me) to great slice of pie! Thank you also for helping write the story that will fuel my success, wink to your podcast nb 6 :-)

    I faced this challenge to pass data between the presenting view controller and the presented view controller. Say if I wanted to change the paragraph “This is the other thing about persistence: …” at runtime with text from a data source, how will I go about that?

    1. You are welcome for that rather detailed explanation. Given how hard it was for me to put it together, I have a feeling you are not the only one. Glad you heard #6. Episodes 7 and 8 are going to be a lot of fun in writing that story.

      As far as passing data It’s the same as any other modal. Transitions do not affect data passing between modals — they actually have nothing to do with each other. Assign it before you present. Remember the outlets don’t exist until after the controller loads. Assign your values to properties, and then in viewDidLoad, load the values into the labels from the properties. On the ModalViewController class I could add two outlets and two properties for the two labels like this

      class ModalViewController: UIViewController {
          var myTitleText = ""
          var myDescriptionText = ""
          @IBOutlet  var titleText: UILabel!
          @IBOutlet  var descriptionText: UILabel!
          @IBAction func dismissModal(sender: AnyObject) {
              dismissViewControllerAnimated(true, completion: nil)
          }
          
          override func viewDidLoad() {
              super.viewDidLoad()
              if myTitleText != "" {
                  titleText.text = myTitleText
              }
              if myDescriptionText != "" {
                  descriptionText.text = myDescriptionText
              }
          }
          
      }
      
      

      I used some if statements to preserve the original text if I don’t change the text.

      In the ViewController class, assign to the properties before you present

      func presentCustomModal(){
              let helloVC = ModalViewController(nibName: "ModalViewController", bundle: nil)
              helloVC.transitioningDelegate = myTransitioningDelegate
              helloVC.modalPresentationStyle = .Custom
              helloVC.modalTransitionStyle = .CrossDissolve
              helloVC.myTitleText = "Slice of App Pie"
              helloVC.myDescriptionText = "I wanted the head of Donald Duck!"
              presentViewController(helloVC, animated: true, completion: nil)
          }
      
      

      If you are after delegates, head over to https://makeapppie.com/2014/09/04/swift-swift-using-xibs-and-delegates-in-modal-views/

      1. Anselme Kotchap Avatar
        Anselme Kotchap

        Thanks for answer! Figured it out , but using delegates rather.

  2. Thanks for an excellent tutorial with great examples and clear explanation. Keep them coming!

    1. Thank you. That is the plan for some good ones. Local Notifications next week.

  3. Thanks for the great tutorial, helped me .
    I have a search controller in the presented view controller , but the search bar in the tableview header is not visible properly and hides a bit. How can i solve this?

  4. […] プロジェクト無く、ソースのみ 英文説明チュートリアル The Step by Step Guide to Custom Presentation Controllers […]

  5. I see this is old, but it helped me a lot today! Thank you!

    1. You are welcome. I’ll update it this week and write a shorter version for iOS Tips Weekly.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.