Mathew Sanders /

Transitions in Swift (pt 3)

This post starts off where we finished in the last post where we created an animated screen transition animation and looks how we can extend the project so that the transition is interactive, and the animation follows the progress of some gesture (like a swipe) we make on the screen.

Custom menu transition

TL;DR

Like the previous posts, if you want to jump in here’s a finished sample project1 that’s ready for you to start experimenting with.

But come back after to follow the step-by-step process so you can better understand what’s happening and be able to make your own transitions with confidence.

Topics

Interactive Entry

Lets start by updating the project so that as well as being able to tap on the + button to trigger the menu animation we can also drag from the left hand edge of the screen to show the menu.

Add support for interactive transitioning

Our MenuTransitionManager object we made in the previous tutorial already adheres to two protocols:

  • UIViewControllerAnimatedTransitioning; and
  • UIViewControllerTransitioningDelegate

For our transition manager object to support a third protocol:

  • UIViewControllerInteractiveTransitioning

Like we have a flag that keeps track of if our transition is presenting or not, we’ll also add a flag that keeps track of whether the transition is interactive or not.

That part is easy: we just need to add it to the end of the list of all protocols that the class supports:

import UIKit

// update our class definition to include `UIViewControllerInteractiveTransitioning` as one of the protocols that this object adheres to
class MenuTransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate, UIViewControllerInteractiveTransitioning  {

    private var interactive = false

    // rest of class definition excluded... 
}

We’ll also add two more methods that are part of the UIViewControllerTransitioningDelegate protocol that tell iOS what object should be used to perform an interactive transition. We’ll either return the transition manager object if our interactive flag is true or return nil if we’re just performing a regular animated transition.

func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {   
    // if our interactive flag is true, return the transition manager object
    // otherwise return nil
    return self.interactive ? self : nil
}

func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return self.interactive ? self : nil
}

We also need an object that can take responsibility for keeping track of the interactive transition, when it should start, what stage the transition should be shown (based on the current gesture progress) and when it should end.

You could write code to manage this from the ground up, but Apple has provided an object called UIPercentDrivenInteractiveTransition that handles most of the heavy lifting for us.

To take advantage of it, lets make our transition manager a subclass of this object2 by replacing class MenuTransitionManager: NSObject with class MenuTransitionManager: UIPercentDrivenInteractiveTransition

The top of your MenuTransitionManager class should look like this…

import UIKit

// update our class definition to include `UIViewControllerInteractiveTransitioning` as one of the protocols that this object adheres to
class MenuTransitionManager: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate, UIViewControllerInteractiveTransitioning  {

// add another flag that will tell us if the transition being performed in interactive or not
private var interactive = false

//... rest of class definition ...  

}

Our transition manager object is now officially an object that is ready to perform interactive transitions. But running the project1 now you won’t see any noticeable difference because we don’t have any gestures set up that the transition can interact with.

Refactor project to prepare for gestures

At the moment, our transition manager is created in the view controller for our menu.

For a simple menu animation it seemed like a good place to define it, but we want to create a gesture that’s performed on our ‘main’ view controller which then interacts with the transition manager.

That’s going to be a bit easier if we refactor our code a little so that the transition manager is created and managed from the main view controller.

Move the creation of the transition manager to the ‘MainViewController’ class and override the prepareForSegue method so that the transition delegate is set here for the menu view controller instead.

Your MainViewController class should look like this:

import UIKit

class MainViewController: UITableViewController {
    
    // create the transition manager object 
    var transitionManager = MenuTransitionManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
       
        let menu = segue.destinationViewController as MenuViewController
        menu.transitioningDelegate = self.transitionManager
        
    }
    
    @IBAction func unwindToMainViewController (sender: UIStoryboardSegue){
        // bug? exit segue doesn't dismiss so we do it manually... 
        self.dismissViewControllerAnimated(true, completion: nil)
    
    }

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

Again, running the project now1 shouldn’t have any noticeable difference. All we’ve done is shuffled some code around to make the next step easier3.

Add entry gesture recognizer

Still in our transition manager class, create two new variables: one for the gesture recognizer, and one for the view controller that the gesture recognizer will be working within4.

/* MenuTransitionManager */

// private so can only be referenced within this object
private var enterPanGesture: UIScreenEdgePanGestureRecognizer!

// not private, so can also be used from other objects :)
var sourceViewController: UIViewController!

Now jump back to the main view controller class and use the new sourceViewController to create a reference to this view controller.

/* MainViewController */

override func viewDidLoad() {
    super.viewDidLoad()
    
    // now we'll have a handy reference to this view controller 
    // from within our transition manager object  
    self.transitionManager.sourceViewController = self
}

Jump back to MenuTransitionManager and I’ll show you something really cool about Swift as a language.

We’re going to add an observer to our sourceViewController property. With the following syntax we can create a mini block of code that will get executed anytime that variable is updated.

We’ll use this so that anytime the sourceViewController is updated we initialize our gesture recognizer and add it to the main view of the property.

/* MenuTransitionManager */

private var enterPanGesture: UIScreenEdgePanGestureRecognizer!

var sourceViewController: UIViewController! {
    didSet {        
        self.enterPanGesture = UIScreenEdgePanGestureRecognizer()
        self.enterPanGesture.addTarget(self, action:"handleOnstagePan:")
        self.enterPanGesture.edges = UIRectEdge.Left
        self.sourceViewController.view.addGestureRecognizer(self.enterPanGesture)
    }
}

// TODO: We need to complete this method to do something useful
func handleOnstagePan(pan: UIPanGestureRecognizer){
     println("Todo: handle onstage gesture...")
}

We also created a method to handle the gesture when it’s triggered, although running at this point1 won’t have any effect other then printing out a bunch of messages in Xcode’s debugging area.

The general approach with connecting a gesture to a transition to make it interactive is to do three things:

  1. Tell the transition when to start
  2. Tell the transition how far through the transition it should be (from 0 to 100%)
  3. Tell the transition when to complete

Now in our method that handles updates from our gesture recognizer we can take advantage of methods that get access to by being a subclass of UIPercentDrivenInteractiveTransition:

func handleOnstagePan(pan: UIPanGestureRecognizer){
    
    // how much distance have we panned in reference to the parent view?
    let translation = pan.translationInView(pan.view!)

    // do some math to translate this to a percentage based value
    let d =  translation.x / CGRectGetWidth(pan.view!.bounds) * 0.5
    
    // now lets deal with different states that the gesture recognizer sends
    switch (pan.state) {

        case UIGestureRecognizerState.Began:
            // set our interactive flag to true
            self.interactive = true

            // trigger the start of the transition
            self.sourceViewController.performSegueWithIdentifier("presentMenu", sender: self)
            break
            
        case UIGestureRecognizerState.Changed:
            
            // update progress of the transition 
            self.updateInteractiveTransition(d)
            break
            
        default: // .Ended, .Cancelled, .Failed ... 
		
            // return flag to false and finish the transition            
            self.interactive = false
            self.finishInteractiveTransition()
    }
}

Running the project now the app would crash if you attempted to make an interactive transition because we’re asking iOS to perform the segue like this: self.sourceViewController.performSegueWithIdentifier("presentMenu", sender: self) but at the moment our segues don’t have any identifiers.

To fix this, open up your storyboard, click on the segue to select it and enter a name to identify the segue.

The name can be anything, but you have to be careful that you don’t reuse that identifier on any other segues in the storyboard: Xcode expects each segue to have a unique identifier.

Running the project1 now, you’ll finally see an interactive transition in progress!

Update to allow cancel transitions

By default, this transition will always complete after a gesture has been started.

Most of the time this isn’t the behavior we want. Instead it’s normally to have some threshold that must be crossed for the transition to complete. Anything below that and the transition should instead be canceled and return the start state.

Start by updating the handleOnstagePan method to only call the finishInteractiveTransition method if d > 0.2.

if(d > 0.2){
    // threshold crossed: finish
    self.finishInteractiveTransition()
}
else {
    // threshold not met: cancel
    self.cancelInteractiveTransition()
}

In our animateTransition method we’ll also need to make some updates to allow for the possibility of the transition to be cancelled.

To do this, we need to replace this:

transitionContext.completeTransition(true)
// bug: we have to manually add our 'to view' back http://openradar.appspot.com/radar?id=5320103646199808
UIApplication.sharedApplication().keyWindow?.addSubview(screens.to.view)

With this5:

// tell our transitionContext object that we've finished animating
if(transitionContext.transitionWasCancelled()){
    
    transitionContext.completeTransition(false)
    // bug: we have to manually add our 'to view' back http://openradar.appspot.com/radar?id=5320103646199808
    UIApplication.sharedApplication().keyWindow?.addSubview(screens.from.view)

}
else {
    
    transitionContext.completeTransition(true)
    // bug: we have to manually add our 'to view' back http://openradar.appspot.com/radar?id=5320103646199808
    UIApplication.sharedApplication().keyWindow?.addSubview(screens.to.view)
    
}

Running the project1 now you’ll be able to see that when you finish the gesture the transition will either continue or cancel depending on how far you’ve panned.

Interactive Exit

Now we have a gesture that can interactively show the menu, lets also add a gesture to interactively dismiss it.

Instead of using a gesture that’s only triggered from the edge of the screen, lets use a pan gesture that can be triggered from panning on any area of the menu screen. Although we’ll make it so that only panning from right to left will dismiss the menu.

Complete to include swipe to exit menu

We need to follow the exact same approach that we’ve just completed to add a second gesture to dismiss the menu screen interactively.

Try this by yourself, but if you get stuck you can review updates made at this step, or download the sample project that includes these updates1.

Add new animation for interactive transition

The transition that we created for a regular transition doesn’t really make sense with our gesture.

Lets change the transition animation so that it stays the same for a regular transition (by tapping on the + or cancel buttons) but has the menu items sliding across the screen for the either of the gestures.

Luckily when we refactored the animation code with helper methods in the previous post, we made this a lot easier for us to do now.

Create a new helper method to manage the offstaging position for elements on the menu screen when we have an interactive transition, and use our interactive flag to check which helper method we should use in the animateTransition method.

Again, I encourage you to try this on your own, but to check the updates made at this step, or download an updated project1 with these changes if you get stuck.

Add status bar background

Finally, it’s annoying that the status bar isn’t visible at some parts of the main view controller when the background is white.

Lets update our transition manager to include a simple UIView that’s the color of the sourceViewController backgroundColor.

A good place to do this is the same place we create and set up the gesture recognizer for our sourceViewController.

private var statusBarBackground: UIView!

var sourceViewController: UIViewController! {
    didSet {
        
        self.enterPanGesture = UIScreenEdgePanGestureRecognizer()
        
        self.enterPanGesture.addTarget(self, action:"handleOnstagePan:")
        self.enterPanGesture.edges = UIRectEdge.Left
        self.sourceViewController.view.addGestureRecognizer(self.enterPanGesture)
        
        // create view to go behind status bar
        self.statusBarBackground = UIView()
        self.statusBarBackground.frame = CGRect(x: 0, y: 0, width: self.sourceViewController.view.frame.width, height: 20)
        self.statusBarBackground.backgroundColor = self.sourceViewController.view.backgroundColor
        
        // add to window rather than view controller
        UIApplication.sharedApplication().keyWindow?.addSubview(self.statusBarBackground)
    }
}

Then in our animation helper methods we can change the background color of this view from to black when we present the menu view controller.

But remember: anything we want to animate in the transition, we need to add to the container view that the animation is performed in6:

// add the both views to our view controller
container.addSubview(menuView)
container.addSubview(topView)
container.addSubview(self.statusBarBackground)

Then in the completion block of the animation, add it back to the window of the app:

UIApplication.sharedApplication().keyWindow.addSubview(self.statusBarBackground)

Running the project1 with these updates shows the minor triumph of the status bar content always being visible.

Make a hamburger

Because we’ve abstracted out the idea of what the menu animation and transition looks like, it’s not such a big deal to switch to a completely different approach.

For example, if we wanted to look at the transitional hamburger button that triggers a menu that appears below the main view controller as we slide it away, we can.

Again, instead of showing you every step needed to make these changes, try own your own and refer back to the updated project1 at this step if you get stuck.

Next Steps

Interactive transitions are a great way to make your apps feel more personal, grounded, and natural — and also open up the possibility for a cleaner app with less chrome.

But they take a lot of experimenting to find the right gestures for the right interaction model so it’s important to explore a range of ideas to find the right idea.

Next post I’m planning to show how you can make a transition where an element that exists on both the source and destination screens in the transition is transformed smoothly throughout the animation - an approach that Android appears to be pushing in their Material Design principles.

Follow me at @permakittens to get updates on new posts!

Read More Swift Animation Posts...
Animations
Part 1
Animations
Part 2
Transitions
Part 1
Transitions
Part 2

Notes

  1. If you’re having trouble at any point, here are versions of the project at each step:

    Note: Sample code has been made in Xcode 6 GM release and won’t work in some other versions. Email me if you’re finding any bugs and I can hopefully point you in the right direction!

    Bug: In the final Xcode 6 release there was a change in the keyWindow method. It now returns an optional so you’ll need to add the ? to the function

    // will cause an error in Xcode 6
    UIApplication.sharedApplication().keyWindow.addSubview(someView)
    
    // fix for Xcode 6
    UIApplication.sharedApplication().keyWindow?.addSubview(someView) 
    

    2 3 4 5 6 7 8 9 10

  2. When we subclass an object we say “we’re taking all your functionality and adding even more to it”.

  3. It’s important to realize that there are always going to be many ways to achieve something. Although there are best practices and conventions that are good to follow, there will never be just a single way that will work.

  4. Since we’re not assigning these either of these variables values right away they are optionals (meaning they can either be nil or something else).

    Normally in Swift you’d declare these with a ? after the variable type to show that they’re an optional. But in this case we’ll add an !. This is called ‘implicit unwrapping’ and should only be used when the variables are assumed to exist immediately after they’ve been created.

  5. This is a lot of code because of the bug that we have to add our views back to the application window after the transition. Hopefully this will be fixed and be a lot simpler in a future update of iOS.

  6. In case you’re wondering, when you use addSubview on an object that’s already within another view it’s automatically removed before added to the new view.