// // SwipeRightToPopViewController.swift // SwipeRightToPopController // // Created by Warif Akhand Rishi on 2/19/16. // Copyright © 2016 Warif Akhand Rishi. All rights reserved. // // Modified by Joseph Hinkle on 12/1/19. // Modified version allows use in SwiftUI by subclassing UIHostingController. // Copyright © 2019 Joseph Hinkle. All rights reserved. // import SwiftUI fileprivate func < (lhs: T?, rhs: T?) -> Bool { switch (lhs, rhs) { case let (l?, r?): return l < r case (nil, _?): return true default: return false } } fileprivate func > (lhs: T?, rhs: T?) -> Bool { switch (lhs, rhs) { case let (l?, r?): return l > r default: return rhs < lhs } } class SwipeRightToPopViewController: UIHostingController, UINavigationControllerDelegate where Content : View { fileprivate var lazyPopContent: LazyPop? private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition? private var panGestureRecognizer: UIPanGestureRecognizer! private var parentNavigationControllerToUse: UINavigationController? private var gestureAdded = false override func viewDidLayoutSubviews() { // You need to add gesture events after every subview layout to protect against weird edge cases // One notable edgecase is if you are in a splitview in landscape. In this case, there will be // no nav controller with 2 vcs, so our addGesture will fail. After rotating back to portrait, // the splitview will combine into one view with the details pushed on top. So only then would // would the addGesture find a parent nav controller with 2 view controllers. I don't know if // there are other edge cases, but running addGesture on every viewDidLayoutSubviews seems safe. addGesture() } public func addGesture() { if !gestureAdded { // attempt to find a parent navigationController var currentVc: UIViewController = self while true { if (currentVc.navigationController != nil) && currentVc.navigationController?.viewControllers.count > 1 { parentNavigationControllerToUse = currentVc.navigationController break } guard let parent = currentVc.parent else { return } currentVc = parent } guard parentNavigationControllerToUse?.viewControllers.count > 1 else { return } panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(SwipeRightToPopViewController.handlePanGesture(_:))) self.view.addGestureRecognizer(panGestureRecognizer) gestureAdded = true } } @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) { // if the parentNavigationControllerToUse has a width value, use that because it's more accurate. Otherwise use this view's width as a backup let total = parentNavigationControllerToUse?.view.frame.width ?? view.frame.width let percent = max(panGesture.translation(in: view).x, 0) / total switch panGesture.state { case .began: if lazyPopContent?.isEnabled == true { parentNavigationControllerToUse?.delegate = self _ = parentNavigationControllerToUse?.popViewController(animated: true) } case .changed: if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition { percentDrivenInteractiveTransition.update(percent) } case .ended: let velocity = panGesture.velocity(in: view).x // Continue if drag more than 50% of screen width or velocity is higher than 100 if percent > 0.5 || velocity > 100 { percentDrivenInteractiveTransition?.finish() } else { percentDrivenInteractiveTransition?.cancel() } case .cancelled, .failed: percentDrivenInteractiveTransition?.cancel() default: break } } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return SlideAnimatedTransitioning() } func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { parentNavigationControllerToUse?.delegate = nil navigationController.delegate = nil if panGestureRecognizer.state == .began { percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition() percentDrivenInteractiveTransition?.completionCurve = .easeOut } else { percentDrivenInteractiveTransition = nil } return percentDrivenInteractiveTransition } } // // Lazy Pop SwiftUI Component // // Created by Joseph Hinkle on 12/1/19. // Copyright © 2019 Joseph Hinkle. All rights reserved. // fileprivate struct LazyPop: UIViewControllerRepresentable { let rootView: Content @Binding var isEnabled: Bool init(_ rootView: Content, isEnabled: (Binding)? = nil) { self.rootView = rootView self._isEnabled = isEnabled ?? Binding(get: { return true }, set: { _ in }) } func makeUIViewController(context: Context) -> UIViewController { let vc = SwipeRightToPopViewController(rootView: rootView) vc.lazyPopContent = self return vc } func updateUIViewController(_ uiViewController: UIViewController, context: Context) { if let host = uiViewController as? UIHostingController { host.rootView = rootView } } } extension View { public func lazyPop(isEnabled: (Binding)? = nil) -> some View { return LazyPop(self, isEnabled: isEnabled) } }