Swift : UIPercentDrivenInteractiveTransition on cancel?
Asked Answered
S

2

8

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)
        }
        )
    }
}
Sidesman answered 13/7, 2017 at 9:3 Comment(0)
S
0

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.

Sidesman answered 24/7, 2017 at 5:46 Comment(0)
N
0

It is possible to know whether the animation was cancelled, and it can be caught in the func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) method from UINavigationControllerDelegate.

Here's a snippet of code on how to do so:

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    navigationController.transitionCoordinator?.notifyWhenInteractionEnds { context in
        if context.isCancelled {
            // The interactive back transition was cancelled
        }
    }
}

This method could be put in your MenuNavigationController, in which you could persist your PresentMenuAnimator and tell it that the transition was cancelled, and in there remove the snapshot that's hanging around.

Nicolina answered 17/7, 2017 at 15:51 Comment(2)
Sorry, it doesn't work since the gesture is cancel but the view is still behind. So the event is not triggered.Sidesman
I've somehow missed that since then, but I'm confused as to which events was not triggered? If you can't remember it's ok, just being curious at that point. I had the same issue at that time and I thought this worked wellLippold
S
0

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.

Sidesman answered 24/7, 2017 at 5:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.