从零实现iOS 26液态水滴背景动画:自定义视图/标签栏的实现方案
从零实现iOS 26液态水滴背景动画:自定义视图/标签栏的实现方案
嘿,我太懂你现在的困扰了——已经把玻璃态的自定义tab栏搭得有模有样,但那个跟着选中项“流动”的液态水滴背景就是卡壳了对吧?结合你提到的分段选择器思路,我给你整理了两个最落地的实现方案,一个是蹭苹果原生的现成效果(省心省力),另一个是完全自定义(自由度拉满),你可以按需选:
方案一:利用UISegmentedControl原生液态动效(最快上手)
你提到的用分段选择器的思路真的是个巧办法——iOS 17+的UISegmentedControl原生就带这个液态流动的选中动效,完全不用自己写动画逻辑!你之前疑惑怎么在分段器里用图片,其实超简单,直接给每个segment设置图片就行,甚至可以混合文字和图标。
改造你的现有代码
把你原来的UIStackView+UIButton替换成UISegmentedControl,无缝集成到玻璃态背景里:
import UIKit import SnapKit class ViewController: UIViewController { private let blur = UIVisualEffectView() private let segmentedControl = UISegmentedControl() override func viewDidLoad() { super.viewDidLoad() // 1. 配置分段器的图标内容 let iconNames = ["person.2.fill", "person.fill", "bell.fill"] let config = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium, scale: .large) for (index, name) in iconNames.enumerated() { let img = UIImage(systemName: name, withConfiguration: config)? .withTintColor(.label, renderingMode: .alwaysOriginal) // 给每个segment设置图标 segmentedControl.setImage(img, forSegmentAt: index) // 隐藏segment默认文字(只保留图标) segmentedControl.setTitle(nil, forSegmentAt: index) } // 2. 开启原生液态动效(iOS 17+默认支持,只需配置样式) if #available(iOS 17.0, *) { segmentedControl.selectedSegmentStyle = .prominent // 调整选中指示器的透明度,模拟液态通透感 segmentedControl.selectedSegmentTintColor = .systemBlue.withAlphaComponent(0.3) } else { // 旧系统兼容方案 segmentedControl.selectedSegmentTintColor = .systemBlue } // 3. 玻璃背景配置(和你原来的代码一致) if #available(iOS 17.0, *) { let glassEffect = UIGlassEffect(style: .regular) glassEffect.isInteractive = true blur.effect = glassEffect } else { blur.effect = UIBlurEffect(style: .regular) } blur.layer.cornerCurve = .continuous blur.contentView.addSubview(segmentedControl) // 4. 布局配置(用你熟悉的SnapKit) segmentedControl.snp.makeConstraints { make in make.edges.equalToSuperview().inset(4) } view.addSubview(blur) blur.snp.makeConstraints { make in make.center.equalToSuperview() } // 5. 监听选中事件,处理tab切换逻辑 segmentedControl.addTarget(self, action: #selector(segmentSelected(_:)), for: .valueChanged) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() blur.layer.cornerRadius = blur.frame.height / 2 } @objc private func segmentSelected(_ sender: UISegmentedControl) { let selectedIndex = sender.selectedSegmentIndex // 这里写你的tab切换逻辑,比如切换子控制器、更新状态等 print("选中了第\(selectedIndex)个tab") } }
这个方案的优势
- 零动画成本:苹果已经把液态流动的交互动效打磨到极致了,点击、滑动切换的过渡丝滑得一批
- 图标配置灵活:不管是SF Symbols还是自定义图片,直接塞进去就行,还能单独调整每个segment的样式
- 风格统一:和你原来的
UIGlassEffect玻璃背景完美兼容,视觉上完全不割裂
方案二:完全自定义液态路径动画(自由度拉满)
如果你不想依赖UISegmentedControl,想完全控制动画的细节(比如水滴的形状、流动速度、颜色变化),可以用CAShapeLayer结合贝塞尔曲线动画来实现。
核心思路
- 用
UIBezierPath绘制水滴形状的路径,选中不同tab时,动态更新路径的控制点,让路径平滑过渡 - 用
CAShapeLayer作为路径的载体,设置填充色、模糊效果模拟液态质感 - 监听按钮点击,触发路径的
path属性动画,实现流动效果
集成到你的现有tab栏
在你原来的UIStackView+UIButton基础上,叠加一个自定义的液态背景层:
import UIKit import SnapKit class ViewController: UIViewController { private let blur = UIVisualEffectView() private let liquidBackgroundLayer = CAShapeLayer() private var selectedTabIndex = 0 private let tabIcons = ["person.2.fill", "person.fill", "bell.fill"] override func viewDidLoad() { super.viewDidLoad() // 1. 保留你原来的按钮栈配置 let stackView = UIStackView() stackView.spacing = 0 stackView.axis = .horizontal for (index, name) in tabIcons.enumerated() { let button = UIButton() let config = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium, scale: .large) let img = UIImage(systemName: name, withConfiguration: config)? .withTintColor(.label, renderingMode: .alwaysOriginal) button.setImage(img, for: .normal) button.snp.makeConstraints { make in make.size.equalTo(CGSize(width: 94, height: 54)) } button.tag = index button.addTarget(self, action: #selector(tabButtonTapped(_:)), for: .touchUpInside) stackView.addArrangedSubview(button) } // 2. 配置液态背景层 liquidBackgroundLayer.fillColor = UIColor.systemBlue.withAlphaComponent(0.3).cgColor liquidBackgroundLayer.cornerCurve = .continuous // 把液态层放在最底层 blur.contentView.layer.insertSublayer(liquidBackgroundLayer, at: 0) // 3. 玻璃背景和栈视图的配置(和原来一致) if #available(iOS 17.0, *) { let effect = UIGlassEffect(style: .regular) effect.isInteractive = true blur.effect = effect } else { blur.effect = UIBlurEffect(style: .regular) } blur.layer.cornerCurve = .continuous blur.contentView.addSubview(stackView) stackView.snp.makeConstraints { make in make.edges.equalToSuperview().inset(4) } view.addSubview(blur) blur.snp.makeConstraints { make in make.center.equalToSuperview() } // 4. 初始化液态背景的位置 updateLiquidBackgroundPath(for: selectedTabIndex, animated: false) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() blur.layer.cornerRadius = blur.frame.height / 2 // 布局变化时同步更新液态路径 updateLiquidBackgroundPath(for: selectedTabIndex, animated: false) } @objc private func tabButtonTapped(_ sender: UIButton) { let newIndex = sender.tag guard newIndex != selectedTabIndex else { return } selectedTabIndex = newIndex // 触发平滑的路径过渡动画 updateLiquidBackgroundPath(for: newIndex, animated: true) } // 核心:动态更新液态背景的路径 private func updateLiquidBackgroundPath(for index: Int, animated: Bool) { guard let stackView = blur.contentView.subviews.first as? UIStackView else { return } guard let selectedButton = stackView.arrangedSubviews[index] as? UIButton else { return } // 计算选中按钮在blur视图内的frame let buttonFrame = selectedButton.convert(selectedButton.bounds, to: blur.contentView) // 绘制水滴形状的贝塞尔路径(这里用圆角矩形模拟,你可以改成更圆润的曲线) let path = UIBezierPath(roundedRect: buttonFrame.insetBy(dx: 8, dy: 8), cornerRadius: buttonFrame.height/4) if animated { // 路径过渡动画 let animation = CABasicAnimation(keyPath: "path") animation.fromValue = liquidBackgroundLayer.path animation.toValue = path.cgPath animation.duration = 0.3 animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) liquidBackgroundLayer.add(animation, forKey: "liquidFlow") } // 更新layer的最终路径 liquidBackgroundLayer.path = path.cgPath } }
优化细节
- 如果你想让水滴更“液态”,可以把圆角矩形换成带椭圆弧度的贝塞尔路径,或者用多个控制点的二次/三次曲线
- 给
liquidBackgroundLayer加个UIBlurEffectView的蒙版,让它和玻璃背景融合得更自然 - 调整动画的
timingFunction,比如用CAMediaTimingFunction(controlPoints: 0.4, 0.0, 0.2, 1.0)来模拟更黏糊的液态流动感
最后给你的小建议
你提到的分段选择器思路真的是捷径,原生动效的成熟度比自己写的高太多了,做产品级项目优先选方案一;如果是想练手或者需要定制特殊效果,再用方案二。另外你写的“iOS 26”应该是笔误吧?应该是iOS 17+,毕竟UIGlassEffect是iOS 17才推出的API~




