Create custom visual effects with SwiftUI
SwiftUI & UI Frameworks 进阶 20m

用 SwiftUI 创建自定义视觉效果

Create custom visual effects with SwiftUI

2024年6月10日

在 Apple 官方观看视频

一句话判断

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 视觉效果。

自定义滚动效果部分重新梳理了 scrollTransitionvisualEffect 两个 modifier。前者基于元素在滚动视图中的位置阶段(leading、center、trailing)来应用变换,后者基于元素的几何信息(位置、大小)来做更精细的视觉调整。

Mesh Gradient 是全新的视图类型,用一组带颜色坐标的控制点在网格上做颜色插值,生成比线性/径向渐变更丰富的色彩效果。控制点的位置可以动态调整,配合动画可以做出非常惊艳的色彩流动效果。

自定义转场部分展示了如何用 AnyTransition.modifierTransition 协议创建自己的转场效果,用 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.LineText.RunText.Grapheme,对每个字符应用自定义的绘制逻辑。

代码片段

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)
                }
        }
    }
}

一句话说明:scrollTransitionphase.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);
}

一句话说明:distortionEffectcolorEffectvisualEffect modifier 的新选项,直接把 Metal shader 绑定到 SwiftUI 视图上。maxSampleOffset 要设够大,否则 shader 采样会越界导致画面撕裂。

最佳实践

新项目:Mesh Gradient 应该成为你做品牌化背景和动态色彩的首选方案,比 Core Animation 的 CAGradientLayer 灵活得多。滚动效果优先考虑 scrollTransition,它比手动监听 ScrollView offset 性能好得多(SwiftUI 在内部做了高效的脏区域标记)。

已有项目:如果你有用 UIKit/CA 实现的自定义滚动效果或文字动画,评估一下是否值得迁移到 SwiftUI 原生方案。scrollTransitionTextRenderer 的声明式写法比手动管理 CA keyframe animation 代码量少很多,可读性也更好。但涉及 Metal shader 的部分需要评估兼容性——有些高级效果可能需要 visualEffect 的 Metal 集成才能实现。

还有什么值得关注

  • visualEffect modifier 在滚动视图里使用时性能很好,因为 SwiftUI 会在渲染管线里做优化,不会每帧都重建视图树。
  • 自定义 Transition 配合 matchedGeometryEffect 可以做出非常流畅的页面转场动画,比 navigationTransition 更灵活。
  • Session 建议对视觉效果要”长期使用测试”——刚做出来的效果很惊艳,但用两周后可能就烦了。好的视觉特效应该经得起日常使用的考验。
WWDC 2024