// // InteractivePushTransition.swift // Tusker // // Created by Shadowfacts on 2/19/20. // Copyright © 2020 Shadowfacts. All rights reserved. // import UIKit /// Allows interactively moving forward through the navigation stack after popping /// Based on https://github.com/NSExceptional/TBInteractivePushTransition class InteractivePushTransition: UIPercentDrivenInteractiveTransition { fileprivate let minimumPushVelocityThreshold: CGFloat = 700 fileprivate let minimumPushDistanceThreshold: CGFloat = 0.5 fileprivate let pushAnimationDuration: TimeInterval = 0.35 private(set) weak var navigationController: EnhancedNavigationViewController! private(set) weak var interactivePushGestureRecognizer: UIScreenEdgePanGestureRecognizer! private(set) var interactive = false private(set) weak var pushingViewController: UIViewController? init(navigationController: EnhancedNavigationViewController) { super.init() self.navigationController = navigationController let interactivePushGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:))) self.interactivePushGestureRecognizer = interactivePushGestureRecognizer navigationController.delegate = self interactivePushGestureRecognizer.edges = .right interactivePushGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!) navigationController.view.addGestureRecognizer(interactivePushGestureRecognizer) } @objc func handleSwipeForward(_ recognizer: UIPanGestureRecognizer) { interactive = true let velocity = recognizer.velocity(in: recognizer.view) let translation = recognizer.translation(in: recognizer.view) let dx = -translation.x / navigationController.view.bounds.width let vx = -velocity.x switch recognizer.state { case .began: if let viewController = navigationController.poppedViewControllers.first { pushingViewController = viewController navigationController.poppedViewControllers.removeFirst() navigationController.skipResetPoppedOnNextPush = true navigationController.pushViewController(viewController, animated: true) } else { interactive = false } case .changed: update(dx) case .ended: if (dx > minimumPushDistanceThreshold || vx > minimumPushVelocityThreshold) { finish() } else { cancel() } interactive = false default: cancel() interactive = false } } } extension InteractivePushTransition: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { self.navigationController.onWillShow() } func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if operation == .push, interactive { return self } return nil } func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { if interactive { return self } return nil } } extension InteractivePushTransition: UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return pushAnimationDuration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let toView = transitionContext.view(forKey: .to)! let fromView = transitionContext.view(forKey: .from)! let dimmingView = UIView() dimmingView.backgroundColor = .black dimmingView.alpha = 0 dimmingView.frame = fromView.frame transitionContext.containerView.addSubview(dimmingView) transitionContext.containerView.addSubview(toView) // modify frame of presented view to go from x = to x = 0 var frame = fromView.frame frame.origin.x = frame.width toView.frame = frame frame.origin.x = 0 // we want it linear while interactive, but we want the "ease out" animation // if the user flicks the screen ahrd enough to finish the transition without interaction let options = interactive ? UIView.AnimationOptions.curveLinear : .curveEaseOut let duration = transitionDuration(using: transitionContext) UIView.animate(withDuration: duration, delay: 0, options: options, animations: { // these magic numbers scientifically determined by repeatedly adjusting and comparing to the system animation let translationDistance = -frame.size.width * 0.3 fromView.transform = CGAffineTransform(translationX: translationDistance, y: 0) toView.frame = frame dimmingView.alpha = 0.075 }) { (finished) in fromView.transform = .identity dimmingView.removeFromSuperview() transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } }