用 SwiftUI 创建自定义视觉效果
Create custom visual effects with SwiftUI
2024年6月10日
一句话判断
SwiftUI 今年给了你 Mesh Gradient、scroll transition 增强、TextRenderer 和 Metal shader 直接写 view modifier 这四把刷子——以前需要手动管理 Core Animation layer 或写大量 UIKit bridge 的视觉效果,现在全部声明式搞定。
这场 Session 讲了什么
这场 Session 由 Philip 和 Rob 联合主讲,聚焦于 SwiftUI 的视觉表达能力扩展。内容覆盖五个主题:自定义滚动效果、Mesh Gradient、自定义转场、TextRenderer、Metal shader 视觉效果。
自定义滚动效果部分重新梳理了 scrollTransition 和 visualEffect 两个 modifier。前者基于元素在滚动视图中的位置阶段(leading、center、trailing)来应用变换,后者基于元素的几何信息(位置、大小)来做更精细的视觉调整。
Mesh Gradient 是全新的视图类型,用一组带颜色坐标的控制点在网格上做颜色插值,生成比线性/径向渐变更丰富的色彩效果。控制点的位置可以动态调整,配合动画可以做出非常惊艳的色彩流动效果。
自定义转场部分展示了如何用 AnyTransition.modifier 和 Transition 协议创建自己的转场效果,用 onAppear/onDisappear 精确控制转场时机。
TextRenderer 让你可以自定义文字的渲染方式——比如逐字符应用颜色、位置偏移、甚至是 3D 变换。
Metal shader 部分展示了如何用 visualEffect modifier 直接调用 Metal shader,实现波浪扭曲、色差等高级图形效果。
值得深挖的点
Mesh Gradient:不只是”好看的渐变”
Mesh Gradient 的核心数据结构是一个网格,每个网格点有一个 SIMD2 坐标和一个颜色值。SwiftUI 在相邻点之间做颜色插值,形成平滑的渐变。跟传统的线性渐变不同,Mesh Gradient 可以在二维平面上产生任意方向的色彩过渡。
实现方式简洁到令人惊讶:
MeshGradient(
width: 3, height: 3,
points: [
.init(0, 0), .init(0.5, 0), .init(1, 0),
.init(0, 0.5), .init(0.5, 0.5), .init(1, 0.5),
.init(0, 1), .init(0.5, 1), .init(1, 1)
],
colors: [/* 9 个颜色 */]
)
移动中间控制点的位置就能改变整个渐变的形态。控制点越近的区域颜色过渡越锐利,越远则越柔和。这个特性让 Mesh Gradient 特别适合做”响应式的”背景——当你的 UI 状态变化时,控制点位置和颜色可以做动画过渡,背景随之流动。
Session 的建议值得采纳:不要害怕把参数调到极端。“Turn the dials up to 100 and make something new”——很多好看的视觉效果都是在极限参数下意外发现的。渐进式的微调反而不容易找到让人眼前一亮的效果。
TextRenderer:文字渲染的终极控制权
TextRenderer 是今年 SwiftUI 最让我兴奋的 API 之一。它让你可以逐字符地控制文字的渲染——每个字符可以有不同的颜色、偏移量、缩放,甚至是时间轴上的动画相位。
一个直接的应用场景是自定义文字转场:比如逐字符淡入、波浪式位移、颜色渐变扫过。过去这些效果需要用 Core Text 手动布局每个 glyph,代码量巨大且难以维护。现在你只需实现 TextRenderer 协议的 draw(layout:in:) 方法,遍历 layout 中的每个 Text.Line、Text.Run、Text.Grapheme,对每个字符应用自定义的绘制逻辑。
代码片段
创建视差滚动效果的 Carousel
ScrollView(.horizontal) {
LazyHStack(spacing: 20) {
ForEach(photos) { photo in
Image(photo.name)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 300, height: 400)
.clipShape(RoundedRectangle(cornerRadius: 20))
.scrollTransition { content, phase in
content
.rotationEffect(.degrees(phase.value * 15))
.scaleEffect(phase.isIdentity ? 1.0 : 0.8)
}
}
}
}
一句话说明:scrollTransition 的 phase.value 从 -1(即将进入)到 0(居中)到 1(即将离开),phase.isIdentity 表示元素在中心位置。坑在于 phase.value 的范围取决于 scrollTransition(axis:animation:) 的配置,默认配置下值域可能不是精确的 -1 到 1。
用 Mesh Gradient 创建动态背景
struct MeshBackground: View {
@State private var isExpanded = false
var body: some View {
MeshGradient(
width: 3, height: 3,
points: [
// 9 个控制点的坐标,中间点会动
.init(0, 0), .init(0.5, 0), .init(1, 0),
.init(0, 0.5),
.init(isExpanded ? 0.7 : 0.3, isExpanded ? 0.7 : 0.3), // 中心点
.init(1, 0.5),
.init(0, 1), .init(0.5, 1), .init(1, 1)
],
colors: [
.purple, .pink, .orange,
.blue, .white, .yellow,
.cyan, .mint, .green
]
)
.animation(.easeInOut(duration: 2).repeatForever(), value: isExpanded)
.onAppear { isExpanded.toggle() }
}
}
一句话说明:Mesh Gradient 的控制点位置变化可以动画化,这是它比普通渐变强大的地方。注意 points 数组的元素数量必须等于 width * height,少了或多了都会崩溃。
用 Metal Shader 创建波浪扭曲效果
// SwiftUI 视图上直接应用 Metal shader
Image("photo")
.visualEffect { content, proxy in
content
.distortionEffect(
ShaderLibrary.wave(
.float(startDate.timeIntervalSinceNow),
.float(0.002) // 波浪频率
),
maxSampleOffset: .init(width: 20, height: 20)
)
}
// 对应的 Metal shader
[[stitchable]] float2 wave(float2 position, float time, float frequency) {
float wave = sin(position.y * frequency + time * 5) * 10;
return position + float2(wave, 0);
}
一句话说明:distortionEffect 和 colorEffect 是 visualEffect modifier 的新选项,直接把 Metal shader 绑定到 SwiftUI 视图上。maxSampleOffset 要设够大,否则 shader 采样会越界导致画面撕裂。
最佳实践
新项目:Mesh Gradient 应该成为你做品牌化背景和动态色彩的首选方案,比 Core Animation 的 CAGradientLayer 灵活得多。滚动效果优先考虑 scrollTransition,它比手动监听 ScrollView offset 性能好得多(SwiftUI 在内部做了高效的脏区域标记)。
已有项目:如果你有用 UIKit/CA 实现的自定义滚动效果或文字动画,评估一下是否值得迁移到 SwiftUI 原生方案。scrollTransition 和 TextRenderer 的声明式写法比手动管理 CA keyframe animation 代码量少很多,可读性也更好。但涉及 Metal shader 的部分需要评估兼容性——有些高级效果可能需要 visualEffect 的 Metal 集成才能实现。
还有什么值得关注
visualEffectmodifier 在滚动视图里使用时性能很好,因为 SwiftUI 会在渲染管线里做优化,不会每帧都重建视图树。- 自定义 Transition 配合
matchedGeometryEffect可以做出非常流畅的页面转场动画,比navigationTransition更灵活。 - Session 建议对视觉效果要”长期使用测试”——刚做出来的效果很惊艳,但用两周后可能就烦了。好的视觉特效应该经得起日常使用的考验。