import UIKit class DetailViewController: UIViewController, UIGestureRecognizerDelegate { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) self.hidesBottomBarWhenPushed = true } required init?(coder: NSCoder) { super.init(coder: coder) self.hidesBottomBarWhenPushed = true } let panGestureRecognizer = UIPanGestureRecognizer.init() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .lightGray panGestureRecognizer.addTarget(self, action: #selector(panGestureRecognizerAction(_:))) panGestureRecognizer.delegate = self view.addGestureRecognizer(panGestureRecognizer); guard let navigationController = navigationController else { return } let title = "Detail Page \(navigationController.viewControllers.count)" self.navigationItem.title = title; let textLabel = UILabel.init(); textLabel.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleBottomMargin, .flexibleRightMargin] textLabel.textAlignment = .center; textLabel.backgroundColor = .init(white: 0.95, alpha: 1.0) textLabel.textColor = UIColor.black; textLabel.font = UIFont.monospacedSystemFont(ofSize: 32.0, weight: .bold) textLabel.text = title textLabel.sizeToFit() let bounds = self.view.bounds; var frame = textLabel.frame; frame.size.width += 40.0; frame.size.height += 40.0; frame.origin.x = (bounds.width - frame.width) * 0.5; frame.origin.y = (bounds.height - frame.height) * 0.5; textLabel.frame = frame; view.addSubview(textLabel); } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) guard let navigationController = navigationController else { return } navigationController.delegate = self; navigationController.interactivePopGestureRecognizer?.isEnabled = false navigationController.interactiveContentPopGestureRecognizer?.isEnabled = false navigationController.interactivePopGestureRecognizer?.require(toFail: panGestureRecognizer) navigationController.interactiveContentPopGestureRecognizer?.require(toFail: panGestureRecognizer) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) } private var animationController: UIViewControllerAnimatedTransitioning? private let interactionController = UIPercentDrivenInteractiveTransition() @objc func panGestureRecognizerAction(_ sender: UIPanGestureRecognizer) { switch sender.state { case .possible: break case .began: break case .changed: let s = self.view.frame.height; let d = sender.translation(in: nil).y; if animationController is HideAnimationController { self.interactionController.update(max(0, d / s)) } else { self.interactionController.update(max(0, -d / s)) } break case .ended: fallthrough case .cancelled: fallthrough case .failed: let s = self.view.frame.height; let d = sender.translation(in: nil).y; let v = sender.velocity(in: nil) let p = animationController is HideAnimationController ? max(0, d / s) : max(0, -d / s) if p > 0.5 || v.y > 500 || v.y < -500 { self.interactionController.finish() } else { self.interactionController.cancel() } self.animationController = nil; break case .recognized: fallthrough default: break } } func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard animationController == nil else { return false } guard let navigationController = navigationController else { return false } guard let pan = gestureRecognizer as? UIPanGestureRecognizer else { return false } let translation = pan.translation(in: nil) let velocity = pan.velocity(in: nil) print("gestureRecognizerShouldBegin: translation = \(translation), velocity = \(velocity)") guard abs(translation.x) <= 0.1 || (translation.y / translation.x) > 3.0 else { return false } if translation.y > 0 { if navigationController.viewControllers.count <= 1 { return false } self.animationController = HideAnimationController() navigationController.popViewController(animated: true) } else { self.animationController = ShowAnimationController() self.interactionController.update(0) let viewController = DetailViewController.init() navigationController.pushViewController(viewController, animated: true) } return true } } extension DetailViewController : UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? { // driven by gesture if let animationController = self.animationController { return animationController } // normal switch operation { case .push: return ShowAnimationController.init() case .pop: return HideAnimationController.init() default: return nil } } func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: any UIViewControllerAnimatedTransitioning) -> (any UIViewControllerInteractiveTransitioning)? { if self.animationController?.isEqual(animationController) == true { return self.interactionController; } return nil } } // pop private class HideAnimationController: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval { return 0.35 } func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView let fromView = transitionContext.view(forKey: .from)! let toView = transitionContext.view(forKey: .to)! let frame = fromView.frame; toView.frame = frame.offsetBy(dx: 0, dy: -frame.height) containerView.insertSubview(toView, belowSubview: fromView) let duration = self.transitionDuration(using: transitionContext) UIView.animate(withDuration: duration) { fromView.frame = frame.offsetBy(dx: 0, dy: frame.height) toView.frame = frame } completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } } // push private class ShowAnimationController: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval { return 0.35 } func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView let fromView = transitionContext.view(forKey: .from)! let toView = transitionContext.view(forKey: .to)! let frame = fromView.frame; toView.frame = frame.offsetBy(dx: 0, dy: frame.height) containerView.addSubview(toView) let duration = self.transitionDuration(using: transitionContext) UIView.animate(withDuration: duration) { fromView.frame = frame.offsetBy(dx: 0, dy: -frame.height) toView.frame = frame } completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } }