Interactive Magic Move

Episode #173 | 17 minutes | published on June 11, 2015
In this episode we take the Magic Move transition from last week's episode and make interactive, so that you can feel the transition along with your swipe.

Episode Links

Creating an Interactive Pan

protocol PanInteractionControllerDelegate {
    func interactiveAnimationDidStart(controller: PanInteractionController)
}

class PanInteractionController : UIPercentDrivenInteractiveTransition {
    var pan: UIPanGestureRecognizer?
    var delegate: PanInteractionControllerDelegate?

    var isActive: Bool {
        get {
            return pan?.state != .Possible
        }
    }

    func attachToView(view: UIView) {
        pan = UIPanGestureRecognizer(target: self, action: Selector("screenEdgePan:"))
        view.addGestureRecognizer(pan!)
    }

    func screenEdgePan(pan: UIScreenEdgePanGestureRecognizer) {
        let view = pan.view!
        switch pan.state {
        case .Began:
            let location = pan.locationInView(view)
            if location.x < CGRectGetMidX(view.bounds) {
                delegate?.interactiveAnimationDidStart(self)
            }

        case .Changed:
            let translation = pan.translationInView(view)
            let percent = fabs(translation.x / CGRectGetWidth(view.bounds))
            updateInteractiveTransition(percent)

        case .Ended:
            if pan.velocityInView(view).x > 0 {
                finishInteractiveTransition()
            } else {
                cancelInteractiveTransition()
            }

        case .Cancelled: fallthrough
        case .Failed:
            cancelInteractiveTransition()

        case .Possible: break

        }
    }

}

Attaching the interaction to DetailViewController

Since the interactive portion of our animation only happens from the DetailViewController, we can have it self contained in this class. If this were a deep hierarchy of view controllers inside of a navigation controller, we'd want to move this to the navigation controller delegate to reuse some code.

First we need to conform to our delegate so we know when the animation starts:

class DetailViewController : UIViewController, 
   UINavigationControllerDelegate, 
   PanInteractionControllerDelegate {
   //...
}

Then we need to change our interaction controller to reference the new class:

  var interactionController = PanInteractionController()

When the view loads, we attach the interaction controller, and set the delegate to self to be notified when the interaction starts.

  override func viewDidLoad() {
        super.viewDidLoad()

        interactionController.delegate = self
        interactionController.attachToView(view)
    }   

}

We also need to tell the navigation controller that we need to pop (animated) when the interaction starts, so that it will actually ask our controller for the proper animation controller and interaction controller for the animation:

    func interactiveAnimationDidStart(controller: PanInteractionController) {
        navigationController?.popViewControllerAnimated(true)
    }

Lastly, we vend the appropriate objects when the navigation controller asks for them:


    func navigationController(navigationController: UINavigationController,
        animationControllerForOperation operation: UINavigationControllerOperation,
        fromViewController fromVC: UIViewController,
        toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

            if operation == .Pop {
                let animator = Animator()
                animator.presenting = false
                return animator
            } else {
                return nil
            }
    }

    func navigationController(navigationController: UINavigationController,
        interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning)
        -> UIViewControllerInteractiveTransitioning? {
            if interactionController.isActive {
                return interactionController
            } else {
                return nil
            }
    }


blog comments powered by Disqus