Search code examples
iosswiftuinavigationcontrollerdelegates

Swift : UIPercentDrivenInteractiveTransition on cancel?


This is my first iOS development and so I am using this tiny project to learn how the system works and how the language (swift) works too.

I am trying to make a drawer menu similar to android app and a certain number of iOS app.

I found this tutorial that explains well how to do it and how it works : here

Now since I am using a NavigationController with show I have to modify the way it is done.

I swapped the UIViewControllerTransitioningDelegate to a UINavigationControllerDelegate so I can override the navigationController function.

This means I can get the drawer out and dismiss it. It works well with a button or with the gesture. My problem is the following : If I don't finish to drag the drawer far enough for it to reach the threshold and finishing the animation, it will be cancel and hidden. This is all well and good but when that happens there is no call to a dismiss function meaning that the snapshot I put in place in the PresentMenuAnimator is still in front of all the layers and I am stuck there even though I can interact with what's behind it.

How can I catch a dismiss or a cancel with the NavigationController ? Is that possible ?

Interactor :

import UIKit


class Interactor:UIPercentDrivenInteractiveTransition {
    var hasStarted: Bool = false;
    var shouldFinish: Bool = false;

}

MenuHelper :

import Foundation
import UIKit

enum Direction {
    case Up
    case Down
    case Left
    case Right
}

struct MenuHelper {
    static let menuWith:CGFloat = 0.8;
    static let percentThreshold:CGFloat = 0.6;
    static let snapshotNumber = 12345;

    static func calculateProgress(translationInView:CGPoint, viewBounds:CGRect, direction: Direction) -> CGFloat {
        let pointOnAxis:CGFloat;
        let axisLength:CGFloat;

        switch direction {
        case .Up, .Down :
            pointOnAxis = translationInView.y;
            axisLength = viewBounds.height;
        case .Left, .Right :
            pointOnAxis = translationInView.x;
            axisLength = viewBounds.width;
        }
        let movementOnAxis = pointOnAxis/axisLength;
        let positiveMovementOnAxis:Float;
        let positiveMovementOnAxisPercent:Float;

        switch direction {
        case .Right, .Down:
            positiveMovementOnAxis = fmaxf(Float(movementOnAxis), 0.0);
            positiveMovementOnAxisPercent = fminf(positiveMovementOnAxis, 1.0);
            return CGFloat(positiveMovementOnAxisPercent);
        case .Left, .Up :
            positiveMovementOnAxis = fminf(Float(movementOnAxis), 0.0);
            positiveMovementOnAxisPercent = fmaxf(positiveMovementOnAxis, -1.0);
            return CGFloat(-positiveMovementOnAxisPercent);
        }
    }

    static func mapGestureStateToInteractor(gestureState:UIGestureRecognizerState, progress:CGFloat, interactor: Interactor?, triggerSegue: () -> Void ) {
        guard let interactor = interactor else {return };

        switch gestureState {
        case .began :
            interactor.hasStarted = true;
             interactor.shouldFinish = false;
            triggerSegue();
        case .changed :
            interactor.shouldFinish = progress > percentThreshold;
            interactor.update(progress);
        case .cancelled :
            interactor.hasStarted = false;
             interactor.shouldFinish = false;
            interactor.cancel();
        case .ended :
            interactor.hasStarted = false;
            interactor.shouldFinish
            ? interactor.finish()
            : interactor.cancel();
             interactor.shouldFinish = false;
        default :
            break;

        }
    }
}

MenuNavigationController :

import Foundation
import UIKit

class MenuNavigationController: UINavigationController, UINavigationControllerDelegate {

    let interactor = Interactor()

    override func viewDidLoad() {
        super.viewDidLoad();

        self.delegate = self;
    }

    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        if((toVC as? MenuViewController) != nil) {
            return PresentMenuAnimator();
        }
        else {
            return DismissMenuAnimator();
        }
    }


    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactor.hasStarted ? interactor : nil;
    }


}

PresentMenuAnimator :

import UIKit

class PresentMenuAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.6;
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard
        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
        let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
            else {return};
        let containerView = transitionContext.containerView;

        containerView.insertSubview(toVC.view, aboveSubview: fromVC.view);

        let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false);
        snapshot?.tag = MenuHelper.snapshotNumber;
        snapshot?.isUserInteractionEnabled = false;
        snapshot?.layer.shadowOpacity = 0.7;
        containerView.insertSubview(snapshot!, aboveSubview: toVC.view);
        fromVC.view.isHidden = true;

        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       animations: {snapshot?.center.x+=UIScreen.main.bounds.width*MenuHelper.menuWith;},
                       completion: {_ in
                        fromVC.view.isHidden = false;
                        transitionContext.completeTransition(!transitionContext.transitionWasCancelled);}
        );
    }

}

DismissMenuAnimator :

import UIKit

class DismissMenuAnimator : NSObject {
}

extension DismissMenuAnimator : UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.6;
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard
            let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
            else {
                return
            }
        let containerView = transitionContext.containerView;


        let snapshot = containerView.viewWithTag(MenuHelper.snapshotNumber)

        UIView.animate(withDuration: transitionDuration(using: transitionContext),
            animations: {

                snapshot?.frame = CGRect(origin: CGPoint.zero, size: UIScreen.main.bounds.size)
            },
            completion: { _ in
                let didTransitionComplete = !transitionContext.transitionWasCancelled
                if didTransitionComplete {

                    containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
                    snapshot?.removeFromSuperview()
                }
                transitionContext.completeTransition(didTransitionComplete)
        }
        )
    }
}

Solution

  • To fix the problem I added a verification in PresentMenuAnimator to check if it the animation was canceled. If it was then remove the snapshot in the UIView.Animate.