You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

从零实现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结合贝塞尔曲线动画来实现。

核心思路

  1. UIBezierPath绘制水滴形状的路径,选中不同tab时,动态更新路径的控制点,让路径平滑过渡
  2. CAShapeLayer作为路径的载体,设置填充色、模糊效果模拟液态质感
  3. 监听按钮点击,触发路径的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~

火山引擎 最新活动