如何为导航控制器的Push/Pop视图控制器添加滑动动画效果?
我来帮你搞定这个滑动切换的需求!你想要的这种滑动导航栏就能完成Push/Pop的效果,其实有两种成熟的实现思路,我给你一步步拆解清楚:
这个方案更贴合原生导航栈的逻辑,适合有层级关系的页面(比如列表页→详情页),你之前尝试的代理方法方向是对的,只是动画逻辑和手势触发需要调整:
1. 自定义页面过渡动画
先写一个符合UIViewControllerAnimatedTransitioning协议的动画类,实现左右滑动的切换效果:
class SlideTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning { private let isPush: Bool init(isPush: Bool) { self.isPush = isPush super.init() } // 动画时长 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.35 } // 核心动画逻辑 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let container = transitionContext.containerView guard let fromVC = transitionContext.viewController(forKey: .from), let toVC = transitionContext.viewController(forKey: .to) else { transitionContext.completeTransition(false) return } let screenWidth = UIScreen.main.bounds.width // 初始化目标页面的位置 toVC.view.frame = CGRect(x: isPush ? screenWidth : -screenWidth, y: 0, width: screenWidth, height: container.bounds.height) container.addSubview(toVC.view) // 执行滑动动画 UIView.animate(withDuration: transitionDuration(using: transitionContext)) { fromVC.view.frame = CGRect(x: self.isPush ? -screenWidth : screenWidth, y: 0, width: screenWidth, height: container.bounds.height) toVC.view.frame = container.bounds } _ in fromVC.view.frame = container.bounds transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } }
2. 实现导航控制器代理
创建代理类,在切换时返回自定义动画:
class NavTransitionDelegate: NSObject, UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return SlideTransitionAnimator(isPush: operation == .push) } }
3. 给导航栏添加滑动手势
自定义导航控制器,给导航栏绑定拖拽手势,根据滑动方向触发Push/Pop:
class CustomNavController: UINavigationController { private let panGesture = UIPanGestureRecognizer() private let transitionDelegate = NavTransitionDelegate() override func viewDidLoad() { super.viewDidLoad() self.delegate = transitionDelegate // 给导航栏添加滑动手势 navigationBar.addGestureRecognizer(panGesture) panGesture.addTarget(self, action: #selector(handleNavBarPan(_:))) } @objc private func handleNavBarPan(_ gesture: UIPanGestureRecognizer) { let translation = gesture.translation(in: view) guard gesture.state == .ended else { return } if translation.x > 50 { // 向右滑动 → Pop页面 popViewController(animated: true) } else if translation.x < -50 { // 向左滑动 → Push页面(这里替换成你要跳转的目标VC) let nextVC = TargetViewController() pushViewController(nextVC, animated: true) } } }
方案二:用UIPageViewController实现平级滑动切换
如果你的页面是平级关系(比如多个分类页),UIPageViewController本身就支持左右滑动切换,只需自定义导航栏保持和GIF一致的样式:
1. 构建容器页面+自定义导航栏
创建一个容器VC,顶部放自定义导航栏,下方嵌入UIPageViewController:
class ContainerVC: UIViewController { private let customNavBar = CustomNavigationBar() private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal) private let pageList: [UIViewController] = [HomeVC(), CategoryVC(), ProfileVC()] private var currentPageIndex = 0 override func viewDidLoad() { super.viewDidLoad() setupCustomNavBar() setupPageViewController() bindNavBarGesture() } // 初始化自定义导航栏 private func setupCustomNavBar() { customNavBar.frame = CGRect(x: 0, y: view.safeAreaInsets.top, width: view.bounds.width, height: 44) customNavBar.title = pageList[currentPageIndex].title customNavBar.leftButton.isHidden = currentPageIndex == 0 view.addSubview(customNavBar) } // 初始化PageViewController private func setupPageViewController() { pageVC.dataSource = self pageVC.delegate = self pageVC.setViewControllers([pageList[currentPageIndex]], direction: .forward, animated: false) addChild(pageVC) pageVC.view.frame = CGRect(x: 0, y: customNavBar.frame.maxY, width: view.bounds.width, height: view.bounds.height - customNavBar.frame.height - view.safeAreaInsets.bottom) view.addSubview(pageVC.view) pageVC.didMove(toParent: self) } // 给导航栏绑定滑动手势 private func bindNavBarGesture() { let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleNavBarPan(_:))) customNavBar.addGestureRecognizer(panGesture) } // 处理导航栏滑动逻辑 @objc private func handleNavBarPan(_ gesture: UIPanGestureRecognizer) { let translation = gesture.translation(in: view) guard gesture.state == .ended else { return } if translation.x > 50 && currentPageIndex > 0 { currentPageIndex -= 1 pageVC.setViewControllers([pageList[currentPageIndex]], direction: .reverse, animated: true) updateNavBarState() } else if translation.x < -50 && currentPageIndex < pageList.count - 1 { currentPageIndex += 1 pageVC.setViewControllers([pageList[currentPageIndex]], direction: .forward, animated: true) updateNavBarState() } } // 更新导航栏状态(标题、按钮显示) private func updateNavBarState() { customNavBar.title = pageList[currentPageIndex].title customNavBar.leftButton.isHidden = currentPageIndex == 0 } } // 实现PageViewController的数据源和代理 extension ContainerVC: UIPageViewControllerDataSource, UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = pageList.firstIndex(of: viewController), index > 0 else { return nil } currentPageIndex = index - 1 updateNavBarState() return pageList[currentPageIndex] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let index = pageList.firstIndex(of: viewController), index < pageList.count - 1 else { return nil } currentPageIndex = index + 1 updateNavBarState() return pageList[currentPageIndex] } } // 自定义导航栏类(完全复刻系统导航栏样式) class CustomNavigationBar: UIView { let titleLabel = UILabel() let leftButton = UIButton(type: .system) override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white setupSubviews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupSubviews() { // 标题标签 titleLabel.textAlignment = .center titleLabel.font = UIFont.systemFont(ofSize: 17, weight: .semibold) addSubview(titleLabel) titleLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor) ]) // 返回按钮 leftButton.setTitle("Back", for: .normal) addSubview(leftButton) leftButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ leftButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), leftButton.centerYAnchor.constraint(equalTo: centerYAnchor) ]) } var title: String? { didSet { titleLabel.text = title } } }
两种方案的适用场景
- 如果页面是层级依赖关系(比如列表→详情),优先选方案一,符合原生导航栈的生命周期逻辑;
- 如果页面是平级切换关系(比如多个分类页),方案二更简洁,UIPageViewController自带滑动切换的基础逻辑。
内容的提问来源于stack exchange,提问作者Mitesh Dobareeya
相关产品推荐
相关产品推荐

