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:
Shadowfacts 2020-02-19 22:07:12 -05:00
parent 8be7480755
commit 65d57df949
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
4 changed files with 224 additions and 1 deletions

View File

@ -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;

View File

@ -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)
}
}

View File

@ -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)
}
}
})
}
}

View File

@ -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)
}
}
}