在iOS界面开发中,让两个图形像水银一样靠近时融合、分开时拉丝断裂,这种有机体动画能显著提升交互质感。SwiftUI虽然内置了很多基础形状,但默认并不提供液态融合能力。我们需要借助自定义Shape、路径数学计算以及持续的时间驱动来构造MorphingShape与LiquidBlob效果。核心思路是把形状表示为一组可插值控制点,通过数学函数改变点的半径或位置,从而在每一帧重绘出新的轮廓。

液态融合的数学原理与形状表示
液态融合视觉效果的本质,是当两个圆形或_blob_轮廓的距离小于某个阈值时,它们的边缘会朝彼此弯曲并形成平滑的连接桥。在二维平面上,最直观的表示方式是将形状定义为极坐标下的闭合路径:以中心点为原点,半径r随角度θ变化。对于基础blob,我们可以用多个正弦波叠加来扰动半径,得到不规则但连续的有机轮廓。公式可写为 r(θ) = R + Σ Aᵢ * sin(fᵢ * θ + φᵢ),其中R为基础半径,A为振幅,f为频率。
当存在两个blob时,分别计算出它们各自轮廓上的采样点。若两点分属不同blob且直线距离小于融合半径,则引入一个中间控制点,利用Catmull-Rom或二次贝塞尔曲线将两边缘桥接。这个过程每帧都要重新计算,因此采样点数量要权衡精度与性能,通常32到64个点已足够肉眼平滑。理解这一原理后,我们才能用SwiftUI的Path逐个移动点并调用addLine或addQuadCurve来闭合形状。
另一个关键是噪声函数的使用。单纯正弦波看起来过于规律,像机械波纹。引入基于时间的伪随机相位或Perlin噪声,能让blob像活物一样缓慢蠕动。在SwiftUI中我们可以用单纯的计算属性返回当前时间对应的扰动值,不依赖外部动画库,从而保持代码轻量且易于调试。
用SwiftUI Shape与TimelineView驱动变形
SwiftUI的Shape协议要求实现path(in:)方法,它接收CGRect并返回Path。我们可以创建一个叫MorphingBlob的结构体,遵守Shape并持有时间参数与配置。配合TimelineView(.animation)每秒刷新60次,在闭包里生成当前时间的形状。下面代码展示了基础blob路径生成逻辑,注意所有小于号都做了转义以符合HTML展示要求。
import SwiftUI
struct MorphingBlob: Shape {
var time: Double
var points: Int = 48
var baseRadius: Double = 80
var amplitude: Double = 20
func path(in rect: CGRect) -> Path {
let center = CGPoint(x: rect.midX, y: rect.midY)
var path = Path()
for i in 0..<points {
let angle = Double(i) / Double(points) * 2 * .pi
let noise = sin(angle * 3 + time) * amplitude
let r = baseRadius + noise
let x = center.x + CGFloat(cos(angle) * r)
let y = center.y + CGFloat(sin(angle) * r)
if i == 0 {
path.move(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
}
}
path.closeSubpath()
return path
}
}
struct BlobView: View {
@State private var time: Double = 0
let timer = Timer.publish(every: 1/60, on: .main, in: .common).autoconnect()
var body: some View {
TimelineView(.animation) { context in
let t = context.date.timeIntervalSince1970
MorphingBlob(time: t)
.fill(Color.blue.opacity(0.6))
.frame(width: 200, height: 200)
}
}
}
上面的代码利用TimelineView在每一帧传入最新时间,MorphingBlob根据时间计算扰动,形成持续变形的液态轮廓。如果要做两个blob融合,可以再写一个LiquidBridge形状,接收两个中心点与半径,在path(in:)里先画第一个blob,再判断距离画桥接曲线,最后画第二个blob。使用fill而不是stroke能让重叠区域自然混色,看起来更像液体。
需要注意的是,Shape本身是无状态的,时间变化引起的重绘由父视图驱动。若把Timer直接放在View里,要确保在onReceive里更新state,否则不会触发刷新。TimelineView是更声明式的做法,它把时间作为环境值注入,避免手动管理订阅,也减少了内存泄漏风险。
性能优化与Canvas降级方案
当屏幕上同时存在十几个blob并互相融合时,纯SwiftUI的Shape逐帧构建Path会带来一定CPU压力,尤其在老设备上。此时可以考虑将绘制逻辑迁移到Canvas(iOS 16+),用GraphicsContext直接绘制路径,减少SwiftUI差分计算开销。Canvas的draw方法接收上下文与时间,我们可以用同样数学公式算出点,然后调用ctx.fill()一次完成,避免多层View嵌套。
另一个优化点是降低采样点数量并结合曲线平滑。用addQuadCurve代替addLine能减少所需点数,比如24个点加贝塞尔控制点就能达到之前48个直线的平滑度。同时,融合检测不必每个点两两计算,可先通过包围盒或中心距粗筛,只对邻近区域做精细桥接,从而把复杂度从O(n²)降到近似O(n)。下表对比了不同方案特点:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Shape+TimelineView | 少量blob、iOS 14+ | 代码简单、兼容好 | 多形状时帧率下降 |
| Canvas绘制 | 大量粒子、iOS 16+ | 绘制高效、可控强 | 旧系统不支持 |
| 金属Metal着色器 | 极致特效 | GPU并行、流畅 | 开发成本高 |
实际项目中,若用户群体大部分在iOS 16以上,优先用Canvas封装一个LiquidBlobRenderer,把融合逻辑写成纯函数,方便单元测试。若需支持旧版,则把Shape方案做成默认,并在设备性能较差时减少amplitude更新频率。通过这种分层策略,既能呈现Custom MorphingShape与LiquidBlob的有机动画,又不会拖垮主线程滚动性能。
SwiftUIMorphingShapeLiquidBlob修改时间:2026-08-14 16:21:33