Episode #172

Magic Move

29 minutes
Published on June 4, 2015

This video is only available to subscribers. Get access to this video and 584 others.

In this episode we'll create a custom view controller animation that mimics the Magic Move behavior from keynote, taking one object and animating into its place on the next slide (or view controller).

Episode Links

Creating an Animator class

We start by creating an Animator class that will hold the logic for animating between two view controllers.

class Animator : NSObject, UIViewControllerAnimatedTransitioning {
  func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return 0.6
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
       // ...
    }
}

Our Animator will handle the push and the pop, which will have slightly different semantics. For this we'll need a boolean property to determine if we're presenting or not:

class Animator : NSObject, UIViewControllerAnimatedTransitioning {

    var presenting = false

    // ...

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {   
        if presenting {
            animatePush(transitionContext)
        } else {
            animatePop(transitionContext)
        }
    }

    func animatePush(transitionContext: UIViewControllerContextTransitioning) {
    }

    func animatePop(transitionContext: UIViewControllerContextTransitioning) {
    }
}

Animating the Push

Animating the push will entail:

  • Getting the frame of the selected cell's image view
  • Creating a snapshot view that looks just like the view to be moved (the image view)
  • Positioning the snapshot view just above the source view (while hiding the source & destination view)
  • Animating the snapshot to the final position
  • Animating the rest of the view controller into position
  • Hiding the snapshot and re-showing both image views
    func animatePush(transitionContext: UIViewControllerContextTransitioning) {
        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! as! ViewController
        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! as! DetailViewController
        let container = transitionContext.containerView()

        // we're going to need the frames of the target view, so 
        // make sure it has laid out properly.
        toVC.view.setNeedsLayout()
        toVC.view.layoutIfNeeded()

        let fromImageView = getCellImageView(fromVC)
        let toImageView = toVC.imageView

        let snapshot = fromImageView.snapshotViewAfterScreenUpdates(false)
        fromImageView.hidden = true
        toImageView.hidden = true

        let backdrop = UIView(frame: toVC.view.frame)
        backdrop.backgroundColor = toVC.view.backgroundColor
        container.addSubview(backdrop)
        backdrop.alpha = 0
        toVC.view.backgroundColor = UIColor.clearColor()

        toVC.view.alpha = 0
        let finalFrame = transitionContext.finalFrameForViewController(toVC)
        var frame = finalFrame
        frame.origin.y += frame.size.height
        toVC.view.frame = frame
        container.addSubview(toVC.view)

        snapshot.frame = container.convertRect(fromImageView.frame, fromView: fromImageView)
        container.addSubview(snapshot)

        UIView.animateWithDuration(transitionDuration(transitionContext)
            , animations: {

                backdrop.alpha = 1
                toVC.view.alpha = 1
                toVC.view.frame = finalFrame
                snapshot.frame = container.convertRect(toImageView.frame, fromView: toImageView)

            }, completion: { (finished) in

                toVC.view.backgroundColor = backdrop.backgroundColor
                backdrop.removeFromSuperview()

                fromImageView.hidden = false
                toImageView.hidden = false
                snapshot.removeFromSuperview()

                transitionContext.completeTransition(finished)
        })
    }

Animating the Pop

Animating the pop is very similar, but everything is done in reverse. There's probably an opportunity to factor out some duplicate logic here.

 func animatePop(transitionContext: UIViewControllerContextTransitioning) {
        let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! as! DetailViewController
        let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! as! ViewController
        let container = transitionContext.containerView()

        let fromImageView = fromVC.imageView
        let toImageView = getCellImageView(toVC)

        let snapshot = fromImageView.snapshotViewAfterScreenUpdates(false)
        fromImageView.hidden = true
        toImageView.hidden = true

        let backdrop = UIView(frame: fromVC.view.frame)
        backdrop.backgroundColor = fromVC.view.backgroundColor
        container.insertSubview(backdrop, belowSubview: fromVC.view)
        backdrop.alpha = 1
        fromVC.view.backgroundColor = UIColor.clearColor()

        let finalFrame = transitionContext.finalFrameForViewController(toVC)
        toVC.view.frame = finalFrame

        var frame = finalFrame
        frame.origin.y += frame.size.height
        container.insertSubview(toVC.view, belowSubview: backdrop)

        snapshot.frame = container.convertRect(fromImageView.frame, fromView: fromImageView)
        container.addSubview(snapshot)

        UIView.animateWithDuration(transitionDuration(transitionContext)
            , animations: {

                backdrop.alpha = 0
                fromVC.view.frame = frame
                snapshot.frame = container.convertRect(toImageView.frame, fromView: toImageView)

            }, completion: { (finished) in

                fromVC.view.backgroundColor = backdrop.backgroundColor
                backdrop.removeFromSuperview()

                fromImageView.hidden = false
                toImageView.hidden = false
                snapshot.removeFromSuperview()

                transitionContext.completeTransition(finished)
        })
    }