You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何自定义UINavigationBar在导航控制器转场中的动画效果?

实现自定义导航栏标题动画(自定义转场时)

你提到的这个问题很典型——WWDC 2013确实说明系统默认的UINavigationController自定义转场中,导航栏只会执行淡入淡出动画,但我们可以通过手动控制导航栏子视图的方式突破这个限制,实现标题移动这类自定义动画,同时保持其他元素(比如左侧BarButton)不受影响。下面是具体的实现思路和步骤:

核心思路

系统把导航栏作为一个整体处理转场动画,所以要单独控制标题的动画,我们需要:

  1. 用自定义标题视图替换系统默认的标题,这样能拿到视图的引用进行动画控制;
  2. 在自定义转场动画中,单独对标题视图添加动画逻辑,同时让系统默认的导航栏淡入淡出(或禁用它),实现标题动、其他元素不动的效果。

具体实现步骤

1. 为ViewController设置自定义TitleView

首先,不要用系统的title属性,而是创建一个自定义的UIView作为导航栏的标题视图,这样我们能完全控制它的行为:

class CustomTitleView: UIView {
    // 自定义标题视图的内容,比如UILabel
    private let titleLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 17, weight: .medium)
        label.textColor = .black
        return label
    }()
    
    init(title: String) {
        super.init(frame: .zero)
        titleLabel.text = title
        addSubview(titleLabel)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
            titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

// 在你的ViewController中设置
override func viewDidLoad() {
    super.viewDidLoad()
    let customTitle = CustomTitleView(title: "我的标题")
    navigationItem.titleView = customTitle
}

2. 实现自定义转场动画控制器

接下来,我们需要实现UIViewControllerAnimatedTransitioning协议,创建自定义的转场动画控制器,这里不仅处理ViewController的转场,还要添加标题视图的动画:

class CustomTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    private let isPresenting: Bool
    
    init(isPresenting: Bool) {
        self.isPresenting = isPresenting
        super.init()
    }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.35
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        guard let fromVC = transitionContext.viewController(forKey: .from),
              let toVC = transitionContext.viewController(forKey: .to),
              let fromView = fromVC.view,
              let toView = toVC.view else {
            transitionContext.completeTransition(false)
            return
        }
        
        // 处理ViewController的基础转场动画(比如滑动)
        if isPresenting {
            containerView.addSubview(toView)
            toView.frame = containerView.frame.offsetBy(dx: containerView.bounds.width, dy: 0)
        }
        
        // 获取两个页面的自定义标题视图
        let fromTitleView = fromVC.navigationItem.titleView as? CustomTitleView
        let toTitleView = toVC.navigationItem.titleView as? CustomTitleView
        
        // 设置标题视图的初始状态
        if isPresenting {
            toTitleView?.transform = CGAffineTransform(translationX: 100, y: 0)
            toTitleView?.alpha = 0
        }
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseInOut) {
            // 执行ViewController转场
            if self.isPresenting {
                toView.frame = containerView.frame
                fromView.frame = containerView.frame.offsetBy(dx: -containerView.bounds.width/3, dy: 0)
            } else {
                fromView.frame = containerView.frame.offsetBy(dx: containerView.bounds.width, dy: 0)
                toView.frame = containerView.frame
            }
            
            // 执行标题视图的动画
            if self.isPresenting {
                toTitleView?.transform = .identity
                toTitleView?.alpha = 1
                fromTitleView?.transform = CGAffineTransform(translationX: -50, y: 0)
                fromTitleView?.alpha = 0.5
            } else {
                fromTitleView?.transform = CGAffineTransform(translationX: 100, y: 0)
                fromTitleView?.alpha = 0
                toTitleView?.transform = .identity
                toTitleView?.alpha = 1
            }
        } completion: { _ in
            // 恢复初始状态,避免影响下次转场
            fromTitleView?.transform = .identity
            fromTitleView?.alpha = 1
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    }
}

3. 设置导航控制器的转场代理

最后,让你的导航控制器代理实现UINavigationControllerDelegate的方法,返回我们的自定义动画控制器:

class YourNavigationControllerDelegate: NSObject, UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomTransitionAnimator(isPresenting: operation == .push)
    }
}

// 在你的导航控制器初始化时设置代理
let navController = UINavigationController(rootViewController: YourFirstVC())
navController.delegate = YourNavigationControllerDelegate()

关键注意点

  • 如果你不想让导航栏本身执行淡入淡出,可以在转场动画中强制设置导航栏的alpha = 1,避免系统默认动画影响;
  • 左侧BarButtonItem之所以不动,是因为我们没有对它添加任何动画逻辑,系统默认的淡入淡出可能会有轻微变化,如果完全不想让它动,可以把BarButton也改成自定义视图,保持它的状态不变;
  • 可以利用UIViewControllerTransitionCoordinator来简化动画协调,比如在ViewController的viewWillAppear中添加动画:
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    transitionCoordinator?.animate(alongsideTransition: { [weak self] context in
        self?.navigationItem.titleView?.transform = .identity
    }, completion: nil)
    navigationItem.titleView?.transform = CGAffineTransform(translationX: 100, y: 0)
}

这样就能实现你提到的“导航栏标题移动、左侧按钮不动”的自定义转场效果了。

内容的提问来源于stack exchange,提问作者ximmyxiao

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:49:32