Add interacting pushing to navigation controllers
Allows people to move forward in the navigation stack after popping (making popping a non-destructive action).
This commit is contained in:
parent
8be7480755
commit
65d57df949
|
@ -157,6 +157,8 @@
|
|||
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
|
||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
|
||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
|
||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
|
||||
|
@ -432,6 +434,8 @@
|
|||
D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ComposeStatusReplyView.xib; sourceTree = "<group>"; };
|
||||
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
|
||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
|
||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
|
||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1136,6 +1140,8 @@
|
|||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1584,6 +1590,7 @@
|
|||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
|
||||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||
|
@ -1698,6 +1705,7 @@
|
|||
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */,
|
||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||
D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -48,7 +48,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
if let vc = vc as? UINavigationController {
|
||||
return vc
|
||||
} else {
|
||||
return UINavigationController(rootViewController: vc)
|
||||
return EnhancedNavigationViewController(rootViewController: vc)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// EnhancedNavigationViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 2/19/20.
|
||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class EnhancedNavigationViewController: UINavigationController {
|
||||
|
||||
var poppedViewControllers = [UIViewController]()
|
||||
var skipResetPoppedOnNextPush = false
|
||||
|
||||
private var interactivePushTransition: InteractivePushTransition!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.interactivePushTransition = InteractivePushTransition(navigationController: self)
|
||||
}
|
||||
|
||||
override func popViewController(animated: Bool) -> UIViewController? {
|
||||
if let popped = super.popViewController(animated: animated) {
|
||||
poppedViewControllers.insert(popped, at: 0)
|
||||
return popped
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
|
||||
if let popped = super.popToRootViewController(animated: animated) {
|
||||
poppedViewControllers = popped
|
||||
return popped
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
|
||||
if let popped = super.popToViewController(viewController, animated: animated) {
|
||||
poppedViewControllers.insert(contentsOf: popped, at: 0)
|
||||
return popped
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
|
||||
if skipResetPoppedOnNextPush {
|
||||
skipResetPoppedOnNextPush = false
|
||||
} else {
|
||||
self.poppedViewControllers = []
|
||||
}
|
||||
super.pushViewController(viewController, animated: animated)
|
||||
}
|
||||
|
||||
func onWillShow() {
|
||||
self.transitionCoordinator?.notifyWhenInteractionChanges({ (context) in
|
||||
if context.isCancelled {
|
||||
if self.interactivePushTransition.interactive {
|
||||
// when an interactive push gesture is cancelled, make sure to adding the VC that was being pushed back onto the popped stack so it doesn't disappear
|
||||
self.poppedViewControllers.insert(self.interactivePushTransition.pushingViewController!, at: 0)
|
||||
} else {
|
||||
// when an interactive pop gesture is cancelled (i.e. the user lifts their finger before it triggers),
|
||||
// the popViewController(animated:) method has already been called so the VC has already been added to the popped stack
|
||||
// so we make sure to remove it, otherwise there could be duplicate VCs on the navigation stasck
|
||||
self.poppedViewControllers.remove(at: 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
//
|
||||
// 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 = <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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue