search by tags

for the user

adventures into the land of the command line

Custom Interactive Animations

This was one of the hardest things I’ve ever had to teach myself from stack overflow questions…

If anyone says this is easy, I roll my eyes. They’re probably some kind of iOS developer or something. I’m just a random pleb, which is why I found it hard.

You need to do three things essentially:

Create a custom animation. (EASY)
Create a gesture recogniser. (EASY)
Tie the gesture interaction to the animation. (OVER LEVEL 9000 HARD)

There’s a couple of different ways I came across for achieving this, and so this post is just the one way I was actually able to get working, because I’m a loser. This is the tutorial I followed.

The animation I wanted to simulate is the same one in the iOS settings app.

The first thing to do is make sure your storyboard contains a NavigationController as the initial View Controller.

Step one, custom animation:

//
//  CustomAnimationController.swift
//  CustomTransitions
//
//  Created by Seb on 24/09/2017.
//  Copyright © 2017 Blah. All rights reserved.
//

import UIKit

class CustomAnimationController: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {

    var reverse: Bool = false

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.7
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)!
        let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)!
        let direction: CGFloat = reverse ? -1 : 1
        let offScreenRight = CGAffineTransform(translationX: direction * containerView.frame.width, y: 0)
        let offScreenLeft = CGAffineTransform(translationX: -direction * containerView.frame.width, y: 0)

        toView.transform = offScreenRight
        if #available(iOS 10.0, *) {
            if (toView.center.x >= ((toView.frame.size.width / 2) + containerView.frame.width)) {
                toView.center = CGPoint(x: (toView.center.x - containerView.frame.width), y: toView.frame.size.height / 2)
            }
        }
        containerView.addSubview(toView)

        UIView.animate(
            withDuration: transitionDuration(using: transitionContext),
            delay: 0.0,
            usingSpringWithDamping: 1.0,
            initialSpringVelocity: 1.0,
            options: [],
            animations: {
                fromView.transform = offScreenLeft
                toView.transform = CGAffineTransform.identity
            },
            completion: {
                finished in
                if (transitionContext.transitionWasCancelled) {
                    toView.removeFromSuperview()
                    transitionContext.completeTransition(false)
                }
                else {
                    fromView.removeFromSuperview()
                    transitionContext.completeTransition(true)
                }
            }
        )
    }

}

Step two, gesture recogniser:

//
//  CustomInteractionController.swift
//  CustomTransitions
//
//  Created by Seb on 24/09/2017.
//  Copyright © 2017 Blah. All rights reserved.
//

import UIKit

class CustomInteractionController: UIPercentDrivenInteractiveTransition {

    var navigationController: UINavigationController!
    var shouldCompleteTransition = false
    var transitionInProgress = false
    var completionSeed: CGFloat {
        return 1 - percentComplete
    }

    func attachToViewController(_ viewController: UIViewController) {
        navigationController = viewController.navigationController
        setupGestureRecognizer(viewController.view)
    }

    fileprivate func setupGestureRecognizer(_ view: UIView) {
        view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(CustomInteractionController.handlePanGesture(_:))))
    }

    @objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
        let viewTranslation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
        switch gestureRecognizer.state {
        case .began:
            transitionInProgress = true
            navigationController.popViewController(animated: true)
        case .changed:
            let const = CGFloat(fminf(fmaxf(Float(viewTranslation.x / 750.0), 0.0), 1.0))
            shouldCompleteTransition = const > 0.15
            update(const)
        case .cancelled, .ended:
            transitionInProgress = false
            if !shouldCompleteTransition || gestureRecognizer.state == .cancelled {
                cancel()
            } else {
                finish()
            }
        default:
            print("Swift switch must be exhaustive, thus the default")
        }
    }

}

Step three, tying the interaction to the animation. This only needs to be added to the first view controller in your set. For example, the one directly after the navigation controller:

//
//  MyViewController.swift
//  CustomTransitions
//
//  Created by Seb on 24/09/2017.
//  Copyright © 2017 Blah. All rights reserved.
//

import UIKit

class MyViewController: UIViewController, UINavigationControllerDelegate {

    //for custom screen transitions
    let customAnimationController = CustomAnimationController()
    let customInteractionController = CustomInteractionController()

    override func viewDidLoad() {
        super.viewDidLoad()

        //for custom screen transitions
        navigationController?.delegate = self
    }

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

        if operation == .Push {
            customInteractionController.attachToViewController(toVC)
        }

        customAnimationController.reverse = operation == .Pop
        return customAnimationController
    }

    //for custom screen transitions
    func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return customInteractionController.transitionInProgress ? customInteractionController : nil
    }

}