Group Group Group Group Group Group Group Group Group
raywenderlich.com Forums

Creating Custom UIViewController Transitions

This tutorial will teach you to create custom UIViewController transitions for presenting and dismissing, and how to make them interactive! With pets!


This is a companion discussion topic for the original entry at http://www.raywenderlich.com/110536/custom-uiviewcontroller-transitions

Very nice and complete topic on UIViewController Transitions!

hey…one small query…i am trying to have the push view kind of animation in a normal presentViewController scenario…this is my code for Push UIViewControllerAnimatedTransitioning:-_

class PushTransitionAnimationController: NSObject,UIViewControllerAnimatedTransitioning {
var originFrame:CGRect!

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

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    guard let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
        let containerView = transitionContext.containerView(),
        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
    else{
        return
    }
    
    //let initialFrame = self.originFrame
    //let finalFrame = transitionContext.finalFrameForViewController(toVC)
    
    let snapshot = toVC.view.snapshotViewAfterScreenUpdates(true)
    snapshot.frame = CGRect(x: originFrame.origin.x + originFrame.width, y: originFrame.origin.y, width: originFrame.width, height: originFrame.height)
    //snapshot.transform = CGAffineTransformMakeTranslation(snapshot.frame.width, 0)
    
    //containerView.addSubview(toVC.view)
    containerView.addSubview(snapshot)
    toVC.view.hidden=true
    containerView.addSubview(toVC.view)
    
    let duration = transitionDuration(transitionContext)
    
    /*UIView.animateWithDuration(duration, animations: {
            snapshot.transform = CGAffineTransformMakeTranslation(-snapshot.frame.width, 0)
        }, completion: {finished in
            toVC.view.hidden=false
            snapshot.removeFromSuperview()
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    })*/
    
    UIView.animateKeyframesWithDuration(duration, delay: 0, options: .CalculationModeCubic, animations: {
        UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1, animations: {
            snapshot.transform = CGAffineTransformMakeTranslation(-snapshot.frame.width, 0)
        })
        
        }, completion: {finished in
            toVC.view.hidden=false
            snapshot.removeFromSuperview()
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    })
}

}

while the animation occurs i get a back patch near the top region of the screen…it occurs for a split of a second though…any help will be much appreciated…

Hi Sir,This is very good tutorial and I am a Jr.IOS App Developer…If You Don’t mine Can you Please Provide to me same tutorial In objective-c?
I want Same functionality In my App!

In the following guard statement

guard let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
let containerView = transitionContext.containerView(),
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
return

the assignment let containerView = transitionContext.containerView() gives compiler error as Swift 9.2 no longer returns an optional UIView for .containerView() and so a simple assignment of the let statement outside of the guard is required for the code to compile.

In the last paragraph that it has to call swipeInteractionController.wireToViewController(destinationViewController) to create and add a UIScreenEdgePanGestureRecognizer to the destination view controller, but I found that the presented view controller’s imageView was blank, so I change the swipeInteractionController.wireToViewController(destinationViewController) to

let dispatchTime: DispatchTime = DispatchTime.now() + Double(Int64(0.6 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)

DispatchQueue.main.after(when: dispatchTime, execute: { [weak self] in
self?.swipeInteractionController.wireToViewController(viewController: destinationViewController)
})

and this will shows the image correctly. Did I missed something that make me to adopt a so trick solution?
BTW, I’m using Swift 3 and Xcode 8 beta on macOS 10.11.5.

Thanks for your great article!

Very nice tutorial, I have followed it and now I can say I have a working knowledge of custom transitions.

One thing to point out though, is that you need to consider the transitionWasCancelled() case when finalizing a transitioning animation, in case you are implementing an interactive transition. Most notably you need to rollback some changes you’ve made to the views. If you use snapshots, or any other form of manipulation on original views, you probably are returning to the original state when finishing up, but, one thing is left for you to do which is adding back the source new controller’s view to the container view! for some reason, even if you call cancelInteractiveTransition() it won’t add it back to the container view.

e.g.:

if transitionContext.transitionWasCancelled() {
        containerView.addSubview(fromVC.view)
        transitionContext.completeTransition(false)
      } else {
        transitionContext.completeTransition(true)
      }


Thanks for this wonderful tutorial. Hope more of this comes forward in the future.

Rather than re-adding the fromVC.view as @mohpor suggests in the transitionWasCancelled case, it’s more correct to change the line:

containerView.addSubview(toVC.view)

in the animateTransition: method of FlipDismissAnimationController to:

containerView.insertSubview(toVC.view,atIndex:0)

The reason for this is that the toVC.view should be visually behind the fromVC.view and its snapshot, and when you use addSubview, it puts the toVC.view on top of the fromVC.view, regardless of its visibility, and when you cancel the transition, the snapshot is not “on top” of its associated view, the fromVC.view, so the transition can get confused. Also, the toVC.view needs to be removed if the transition was cancelled.

When doing this tutorial’s flipping animation, you won’t see the confused views show up (at least I didn’t in the environments I was using), but if you change the animations to just a simple grow/shrink/alpha-channel-change, and then test the left-edge-drag dismissal by dragging slightly in and then all the way back out to the left (much easier in the simulator), you see the issue.

In the case of the similar code in FlipPresentAnimationController, it’s fine since the toVC.view needs to be on top, and addSubview ensures that as well as that its snapshot is on top of it.

Essentially, FlipDismissAnimationController.swift could be written this way, along with the animation changes that demonstrate the issue if not changed (see the #if true sections and their associated comments).

//
//  FlipDismissAnimationController.swift
//  GuessThePet
//
//  Created by Vesza Jozsef on 08/07/15.
//  Copyright (c) 2015 Razeware LLC. All rights reserved.
//

import UIKit

class FlipDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

  var destinationFrame = CGRectZero

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

  func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

    guard let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
        let containerView = transitionContext.containerView(),
        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
        return
    }

#if true // Non-flipping animation
    let finalFrame = CGRectInset (destinationFrame,destinationFrame.size.width / 2.0,destinationFrame.size.height / 2.0)
#else // Flipping animation
    let finalFrame = destinationFrame
#endif // Non-flipping or Flipping animation

    let snapshot = fromVC.view.snapshotViewAfterScreenUpdates(false)

    snapshot.layer.cornerRadius = 25
    snapshot.layer.masksToBounds = true
#if true // Non-flipping animation
    snapshot.alpha = 1.0
#endif // Non-flipping or Flipping animation

#if true // Better way to ensure correct order of subviews and the snapshot
    // We need the toVC.view behind the fromVC.view and its snapshot, or
    // things get confused if a transition cancel happens.
    containerView.insertSubview(toVC.view,atIndex:0)
#else // Original way that caused the subviews to be out of order
    containerView.addSubview(toVC.view)
#endif // Better way to ensure correct order of subviews and the snapshot
    containerView.addSubview(snapshot)
    fromVC.view.hidden = true

#if true // Non-flipping animation
#else // Flipping animation
    AnimationHelper.perspectiveTransformForContainerView(containerView)

    toVC.view.layer.transform = AnimationHelper.yRotation(-M_PI_2)
#endif // Non-flipping or Flipping animation

    let duration = transitionDuration(transitionContext)

    UIView.animateKeyframesWithDuration(
      duration,
      delay: 0,
      options: .CalculationModeCubic,
      animations: {
#if true // Non-flipping animation
        UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: {
          snapshot.alpha = 0.0
        })
        UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1.0, animations: {
          snapshot.frame = finalFrame
        })
#else // Flipping animation
        UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 1/3, animations: {
          snapshot.frame = finalFrame
        })
    
        UIView.addKeyframeWithRelativeStartTime(1/3, relativeDuration: 1/3, animations: {
          snapshot.layer.transform = AnimationHelper.yRotation(M_PI_2)
        })
    
        UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: {
          toVC.view.layer.transform = AnimationHelper.yRotation(0.0)
        })
#endif // Non-flipping or Flipping animation
      },
      completion: { _ in
        fromVC.view.hidden = false
        snapshot.removeFromSuperview()
#if true // Need to remove the toVC.view if cancelled
        if transitionContext.transitionWasCancelled()
        {
            toVC.view.removeFromSuperview()
        }
#endif // Need to remove the toVC.view if cancelled
        transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
      })
  }
}

Its really good information to understand about animations.
It will be helpful if the code is ported to Swift 3

doesn’t work well on iPhone 7
instead of cat we have a white uiview during transition animation (snapshot isn’t working)

I have updated the project to Swift 3.1 with XCode 8.3.2.

I’m also looking for its objective-C version. Please also let me know if there’s one and thanks.

This tutorial is more than six months old, so questions are no longer supported at the moment for it. We will update it as soon as possible. Thank you! :]