forked from shadowfacts/Tusker
144 lines
5.9 KiB
Swift
144 lines
5.9 KiB
Swift
//
|
|
// 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)
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|