如何自定义UINavigationBar在导航控制器转场中的动画效果?
实现自定义导航栏标题动画(自定义转场时)
你提到的这个问题很典型——WWDC 2013确实说明系统默认的UINavigationController自定义转场中,导航栏只会执行淡入淡出动画,但我们可以通过手动控制导航栏子视图的方式突破这个限制,实现标题移动这类自定义动画,同时保持其他元素(比如左侧BarButton)不受影响。下面是具体的实现思路和步骤:
核心思路
系统把导航栏作为一个整体处理转场动画,所以要单独控制标题的动画,我们需要:
- 用自定义标题视图替换系统默认的标题,这样能拿到视图的引用进行动画控制;
- 在自定义转场动画中,单独对标题视图添加动画逻辑,同时让系统默认的导航栏淡入淡出(或禁用它),实现标题动、其他元素不动的效果。
具体实现步骤
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
相关产品推荐
相关产品推荐

