如何实现App Store风格导航栏的滚动动画效果?
解决App Store风格导航栏动画的核心问题
我来帮你拆解并解决这两个导航栏动画的关键问题,同时还原原生的半透明导航栏与边框效果:
一、修复滑动返回时导航栏的异常显示问题
你当前在viewWillDisappear中强制设置alpha = 1,但交互式滑动返回是持续的过渡过程,这个时机设置会导致视觉跳变。更平滑的处理方式是:
- 在页面出现时,根据当前滚动位置重新计算导航栏状态;
- 利用导航控制器的转场协调器,在滑动返回过程中同步更新导航栏,避免跳变。
修改代码如下:
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 页面出现时,根据当前滚动位置恢复导航栏状态 scrollViewDidScroll(collectionView) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if let coordinator = navigationController?.transitionCoordinator { // 监听滑动返回的过渡过程 coordinator.animate(alongsideTransition: { context in self.navigationController?.navigationBar.alpha = 1 self.updateNavigationBarAppearance(percentage: 1) }, completion: { context in if context.isCancelled { // 用户取消滑动返回时,恢复当前页面的导航栏状态 self.scrollViewDidScroll(self.collectionView) } }) } else { // 非交互式返回(如点击返回按钮),直接恢复默认状态 navigationController?.navigationBar.alpha = 1 updateNavigationBarAppearance(percentage: 1) } }
二、实现返回按钮的原生切换效果
Apple的原生实现确实是通过叠加两个按钮完成的:一个自定义圆形箭头按钮(导航栏透明时显示),一个系统默认返回按钮(导航栏出现时显示)。具体实现步骤:
- 初始化自定义返回按钮并添加到导航栏;
- 通过
navigationItem.hidesBackButton控制系统按钮的显示/隐藏; - 根据滚动百分比,同步调整两个按钮的透明度,实现平滑过渡。
添加以下代码:
override func viewDidLoad() { super.viewDidLoad() // 创建自定义返回按钮(可替换为你需要的样式) let customBackButton = UIButton(type: .system) customBackButton.setImage(UIImage(systemName: "arrow.left.circle"), for: .normal) customBackButton.tintColor = .white customBackButton.addTarget(self, action: #selector(customBackTapped), for: .touchUpInside) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: customBackButton) // 初始隐藏系统返回按钮 navigationItem.hidesBackButton = true } @objc func customBackTapped() { navigationController?.popViewController(animated: true) } // 修改scrollViewDidScroll方法,加入按钮切换逻辑 func scrollViewDidScroll(_ scrollView: UIScrollView) { let currentVerticalOffset = scrollView.contentOffset.y let maxOffset = collectionView.frame.size.height // 限制百分比在0-1之间,避免超出范围 let percentage = min(max(currentVerticalOffset / maxOffset, 0), 1) // 更新标题显示 title = percentage > 0.5 ? "The Barn Owl" : "" // 更新导航栏透明度 navigationController?.navigationBar.alpha = percentage // 切换返回按钮:自定义按钮随百分比降低透明度,系统按钮随百分比显示 navigationItem.leftBarButtonItem?.customView?.alpha = 1 - percentage navigationItem.hidesBackButton = percentage < 0.5 // 更新导航栏半透明与边框效果 updateNavigationBarAppearance(percentage: percentage) }
三、还原原生半透明导航栏与边框效果
直接调整backgroundColor或alpha会丢失半透明特性和边框,正确的做法是利用导航栏的barTintColor、backgroundImage和shadowImage实现:
func updateNavigationBarAppearance(percentage: CGFloat) { guard let navBar = navigationController?.navigationBar else { return } navBar.isTranslucent = true // 设置半透明白色背景,透明度随滚动百分比变化 navBar.barTintColor = UIColor.white.withAlphaComponent(percentage) // 控制导航栏边框显示:百分比超过阈值时显示边框 // 这里的"navBarShadow"是自定义的边框图片(与系统默认样式一致即可) let shadowImage = percentage > 0.1 ? UIImage(named: "navBarShadow") : UIImage() navBar.shadowImage = shadowImage // 适配文字与按钮颜色,随背景变化 let textColor = percentage > 0.5 ? UIColor.black : UIColor.white navBar.titleTextAttributes = [.foregroundColor: textColor] navBar.tintColor = textColor navigationItem.leftBarButtonItem?.customView?.tintColor = textColor }
完整优化后的代码
整合以上所有逻辑,完整的Swift 5实现代码如下:
import UIKit class OwlViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource { @IBOutlet weak var collectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad() // 初始化自定义返回按钮 let customBackButton = UIButton(type: .system) customBackButton.setImage(UIImage(systemName: "arrow.left.circle"), for: .normal) customBackButton.tintColor = .white customBackButton.addTarget(self, action: #selector(customBackTapped), for: .touchUpInside) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: customBackButton) // 初始隐藏系统返回按钮 navigationItem.hidesBackButton = true // 初始设置导航栏为透明状态 updateNavigationBarAppearance(percentage: 0) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) scrollViewDidScroll(collectionView) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if let coordinator = navigationController?.transitionCoordinator { coordinator.animate(alongsideTransition: { context in self.navigationController?.navigationBar.alpha = 1 self.updateNavigationBarAppearance(percentage: 1) }, completion: { context in if context.isCancelled { self.scrollViewDidScroll(self.collectionView) } }) } else { navigationController?.navigationBar.alpha = 1 updateNavigationBarAppearance(percentage: 1) } } @objc func customBackTapped() { navigationController?.popViewController(animated: true) } func scrollViewDidScroll(_ scrollView: UIScrollView) { let currentVerticalOffset = scrollView.contentOffset.y let maxOffset = collectionView.frame.size.height let percentage = min(max(currentVerticalOffset / maxOffset, 0), 1) title = percentage > 0.5 ? "The Barn Owl" : "" navigationController?.navigationBar.alpha = percentage navigationItem.leftBarButtonItem?.customView?.alpha = 1 - percentage navigationItem.hidesBackButton = percentage < 0.5 updateNavigationBarAppearance(percentage: percentage) } func updateNavigationBarAppearance(percentage: CGFloat) { guard let navBar = navigationController?.navigationBar else { return } navBar.isTranslucent = true navBar.barTintColor = UIColor.white.withAlphaComponent(percentage) let shadowImage = percentage > 0.1 ? UIImage(named: "navBarShadow") : UIImage() navBar.shadowImage = shadowImage let textColor = percentage > 0.5 ? UIColor.black : UIColor.white navBar.titleTextAttributes = [.foregroundColor: textColor] navBar.tintColor = textColor navigationItem.leftBarButtonItem?.customView?.tintColor = textColor } // MARK: - CollectionView DataSource func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 20 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) cell.backgroundColor = .lightGray return cell } }
这样就能完美还原App Store的导航栏动画效果,包括滑动返回的平滑过渡、返回按钮的自然切换,以及半透明导航栏与边框的正确显示。
内容的提问来源于stack exchange,提问作者Junaid




