Instantly share code, notes, and snippets.
Last active
October 21, 2022 06:25
-
Star
(2)
2
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save topas/9af09d298435d18f1094fbe4055ef798 to your computer and use it in GitHub Desktop.
UINavigationController with cancellable swipe back (left) gesture
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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