Tusker/Tusker/Screens/Utilities/InteractivePushTransition.s...

148 lines
5.9 KiB
Swift

//
// InteractivePushTransition.swift
// Tusker
//
// Created by Shadowfacts on 2/19/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
#if !os(visionOS)
/// 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)
let trackpadGestureRecognizer = TrackpadScrollGestureRecognizer(target: self, action: #selector(handleSwipeForward(_:)))
trackpadGestureRecognizer.require(toFail: navigationController.interactivePopGestureRecognizer!)
navigationController.view.addGestureRecognizer(trackpadGestureRecognizer)
}
@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 = <screen width> 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)
}
}
}
#endif