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 */; };
|
D67C57B221E28FAD00C3118B /* ComposeStatusReplyView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D67C57B121E28FAD00C3118B /* ComposeStatusReplyView.xib */; };
|
||||||
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
|
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.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 */; };
|
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; };
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; };
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1136,6 +1140,8 @@
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */,
|
||||||
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
|
||||||
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */,
|
||||||
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */,
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1584,6 +1590,7 @@
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
|
||||||
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
|
0454DDAF22B462EF00B8BB8E /* GalleryExpandAnimationController.swift in Sources */,
|
||||||
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
|
||||||
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
|
||||||
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
|
@ -1698,6 +1705,7 @@
|
||||||
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */,
|
D667E5E721349D4C0057A976 /* ProfileTableViewController.swift in Sources */,
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||||
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||||
D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */,
|
D6163F2C21AA0AF1008DAC41 /* MyProfileTableViewController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -48,7 +48,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
if let vc = vc as? UINavigationController {
|
if let vc = vc as? UINavigationController {
|
||||||
return vc
|
return vc
|
||||||
} else {
|
} 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