Skip to content

Instantly share code, notes, and snippets.

@topas
Last active October 21, 2022 06:25
Show Gist options
  • Save topas/9af09d298435d18f1094fbe4055ef798 to your computer and use it in GitHub Desktop.
Save topas/9af09d298435d18f1094fbe4055ef798 to your computer and use it in GitHub Desktop.
UINavigationController with cancellable swipe back (left) gesture
import UIKit
protocol SwipeBackDelegate: class {
func shouldSwipeBackStarted(point: CGPoint) -> Bool
func swipeBackFinished() -> Bool
func swipeBackNotFinished()
}
class CancelableNavigationController: UINavigationController, UIGestureRecognizerDelegate, UINavigationControllerDelegate {
private var leftEdgeSwipeRecognizer: UIScreenEdgePanGestureRecognizer?
private var swipeTransition: UIPercentDrivenInteractiveTransition?
weak var swipeBackCallbackDelegate: SwipeBackDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
interactivePopGestureRecognizer?.enabled = false
leftEdgeSwipeRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(CancelableNavigationController.swipedLeftEdge(_:)))
leftEdgeSwipeRecognizer!.delegate = self
leftEdgeSwipeRecognizer!.edges = UIRectEdge.Left
view.addGestureRecognizer(leftEdgeSwipeRecognizer!)
}
func swipedLeftEdge(recognizer: UIScreenEdgePanGestureRecognizer) {
let progress = max(recognizer.translationInView(view).x / view.frame.size.width, 0)
let velocity = recognizer.velocityInView(view).x
switch recognizer.state {
case .Began:
popViewControllerAnimated(true)
break
case .Changed:
swipeTransition?.updateInteractiveTransition(progress)
break
case .Cancelled, .Ended:
if progress < 0.5 && velocity < 800 {
swipeBackCallbackDelegate?.swipeBackNotFinished()
swipeTransition?.cancelInteractiveTransition()
break
}
guard let callback = swipeBackCallbackDelegate else {
swipeTransition?.finishInteractiveTransition()
break
}
if callback.swipeBackFinished() {
swipeTransition?.finishInteractiveTransition()
} else {
swipeTransition?.cancelInteractiveTransition()
}
break
default:
break
}
}
override func popViewControllerAnimated(animated: Bool) -> UIViewController? {
return super.popViewControllerAnimated(animated)
}
func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if let swipeRecognizer = gestureRecognizer as? UIScreenEdgePanGestureRecognizer {
let point = swipeRecognizer.locationInView(view)
if !(swipeBackCallbackDelegate?.shouldSwipeBackStarted(point) ?? true) {
return false
}
}
if viewControllers.count > 1 {
return true
}
return false
}
func navigationController(navigationController: UINavigationController,
animationControllerForOperation operation: UINavigationControllerOperation,
fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == .None {
return nil
}
return SlideTransitioning(operation: operation)
}
func navigationController(navigationController: UINavigationController,
interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
guard let navController = navigationController as? CancelableNavigationController else { return nil }
if navController.leftEdgeSwipeRecognizer?.state == .Began {
navController.swipeTransition = UIPercentDrivenInteractiveTransition()
navController.swipeTransition?.completionCurve = .Linear
} else {
navController.swipeTransition = nil
}
return navController.swipeTransition
}
}
class SlideTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
let operation: UINavigationControllerOperation
init(operation: UINavigationControllerOperation) {
self.operation = operation
}
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.3
}
func animateTransition(context: UIViewControllerContextTransitioning) {
let containerView = context.containerView()!
let toView = context.viewControllerForKey(UITransitionContextToViewControllerKey)!.view
let fromView = context.viewControllerForKey(UITransitionContextFromViewControllerKey)!.view
let width = containerView.frame.width
if operation == .Push {
containerView.addSubview(toView)
toView.frame = toView.frame.offsetBy(dx: width, dy: 0)
fromView.layer.opacity = 1
UIView.animateWithDuration(transitionDuration(context), delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: {
toView.frame = fromView.frame
fromView.layer.opacity = 0.7
}, completion: { finished in
context.completeTransition(!context.transitionWasCancelled())
})
}
if operation == .Pop {
containerView.insertSubview(toView, belowSubview: fromView)
toView.layer.opacity = 0.7
UIView.animateWithDuration(transitionDuration(context), delay: 0, options: UIViewAnimationOptions.CurveLinear, animations: {
fromView.frame = containerView.frame.offsetBy(dx: width, dy: 0)
toView.layer.opacity = 1
return
}, completion: { finished in
context.completeTransition(!context.transitionWasCancelled())
})
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment