Highlight
SwiftUI 在 iOS 18 新增了 MeshGradient、TextRenderer、自定义 Transition 协议和 Metal shader 集成,让视觉效果的实现从”只能用内置 modifier”变成”像素级自定义”。
核心内容
做 SwiftUI 应用时,视觉动效常常卡在两件事上:要么内置 modifier 不够用,比如 scrollTransition 只能做旋转缩放,想做色相偏移就得手写 GeometryReader;要么想做文字逐字动画,但 Text 视图是不可拆分的黑盒,拿不到单个 glyph 的位置。
这回 Apple 给了五个新工具来补齐这些缺口。scrollTransition 和 visualEffect 两个 modifier 覆盖了滚动场景下基于位置和基于几何信息的两种视觉调整方式。MeshGradient 用一组带颜色的控制点在网格上做插值,比线性/径向渐变灵活得多,控制点位置还能动画。自定义 Transition 协议让你用 TransitionPhase 区分 willAppear 和 didDisappear,写出有方向感的转场。TextRenderer 协议把 Text.Layout 的 lines、runs、runSlices 全部暴露出来,配合 TextAttribute 标记关键文字,可以逐字做 spring 动画。最后,Metal shader 通过 ShaderLibrary 和 layerEffect 直接集成到 SwiftUI 视图树上,GPU 逐像素计算,性能远超 CPU 方案。
整场 Session 的核心思路是:视觉效果需要反复调试才能到位,所以每个 API 都设计成可声明、可组合、可实时预览的形式,降低从”想法”到”看到效果”的延迟。
详细内容
自定义滚动效果
scrollTransition 暴露一个 phase 参数,值域为 leading / center / trailing,同时提供 phase.value(浮点偏移量)和 phase.isIdentity(是否在中心)两个属性。用它做视差效果时,只偏移图片内容、不偏移裁剪容器,就能实现”窗口透视”的感觉(03:14)。
visualEffect 则基于 GeometryProxy 提供视图的全局位置和尺寸信息,可以做色相旋转、模糊、缩放等基于坐标的视觉变化(04:41):
RoundedRectangle(cornerRadius: 24)
.fill(.purple)
.visualEffect({ content, proxy in
content
.hueRotation(Angle(degrees: proxy.frame(in: .global).origin.y / 10))
})
关键点:
visualEffect的闭包参数proxy是GeometryProxy,可读取视图的 frame、safeAreaInsets 等几何信息- 返回值必须是
some View,且只能用视觉类 modifier(opacity、blur、hueRotation 等),不能改变布局 - 因为不触发 layout pass,性能优于 GeometryReader + 状态驱动的方式
Mesh Gradient
MeshGradient 用 width x height 定义网格,每个控制点指定 SIMD2 坐标和颜色,SwiftUI 在点之间自动插值(07:30):
MeshGradient(
width: 3,
height: 3,
points: [
[0.0, 0.0], [0.5, 0.0], [1.0, 0.0],
[0.0, 0.5], [0.9, 0.3], [1.0, 0.5],
[0.0, 1.0], [0.5, 1.0], [1.0, 1.0]
],
colors: [
.black,.black,.black,
.blue, .blue, .blue,
.green, .green, .green
]
)
关键点:
points数组长度必须等于width * height,每项是[Float, Float],取值 0~1- 中心点
[0.9, 0.3]偏离了正中位置,导致蓝色区域向右上方偏移,这就是 Mesh Gradient 的核心:控制点位置决定颜色分布 - 控制点位置可以绑定
@State,配合withAnimation实现颜色流动
自定义 Transition
iOS 18 新增 Transition 协议,TransitionPhase 区分三种状态:willAppear、identity、didDisappear(10:36):
struct Twirl: Transition {
func body(content: Content, phase: TransitionPhase) -> some View {
content
.scaleEffect(phase.isIdentity ? 1 : 0.5)
.opacity(phase.isIdentity ? 1 : 0)
.blur(radius: phase.isIdentity ? 0 : 10)
.rotationEffect(
.degrees(
phase == .willAppear ? 360 :
phase == .didDisappear ? -360 : .zero
)
)
.brightness(phase == .willAppear ? 1 : 0)
}
}
关键点:
phase.isIdentity为 true 时视图完全显示,其他时候可读phase.value做连续插值phase == .willAppear和phase == .didDisappear让旋转方向可以区分,进入时正转、退出时反转brightness只在进入时加 1,给出场瞬间加高光,吸引注意力
TextRenderer
TextRenderer 协议的核心方法是 draw(layout:in:),Text.Layout 按层级提供 lines -> runs -> runSlices,最小的 runSlice 对应单个 glyph(13:29)。要驱动动画,renderer 遵循 Animatable 协议,animatableData 指向 elapsedTime。配合 TextAttribute 可以标记特定文字范围,让 renderer 只对标记部分做逐字动画,其余部分快速淡入(14:01)。
Metal Shader 集成
Metal shader 函数用 [[ stitchable ]] 标记,SwiftUI 通过 ShaderLibrary.Ripple(...) 按名称调用。Layer Effect 拿到每个像素的 position 和原始图层内容,可以做扭曲、色差等 GPU 级效果(22:55):
[[ stitchable ]]
half4 Ripple(
float2 position,
SwiftUI::Layer layer,
float2 origin,
float time,
float amplitude,
float frequency,
float decay,
float speed
) {
float distance = length(position - origin);
float delay = distance / speed;
time -= delay;
time = max(0.0, time);
float rippleAmount = amplitude * sin(frequency * time) * exp(-decay * time);
float2 n = normalize(position - origin);
float2 newPosition = position + rippleAmount * n;
half4 color = layer.sample(newPosition);
color.rgb += 0.3 * (rippleAmount / amplitude) * color.a;
return color;
}
关键点:
position是当前像素坐标,layer是视图的图层内容,用layer.sample(at:)采样- 波纹从
origin向外传播,distance / speed计算延迟,越远的像素越晚开始抖动 sin(frequency * time) * exp(-decay * time)是阻尼正弦波,时间越长振幅越小color.rgb += 0.3 * ...在波峰处加亮,模拟光线折射
SwiftUI 端用 RippleEffect ViewModifier 包裹 keyframeAnimator,把 elapsedTime 从 0 动画到 duration,每一帧传给 RippleModifier,后者用 visualEffect + layerEffect 调用 shader(23:36)。
核心启发
-
做什么:用
visualEffect做基于滚动位置的色相/模糊渐变列表。为什么值得做:单色列表视觉单调,用proxy.frame(in: .global).origin.y驱动 hueRotation 零行布局代码就能让列表有色彩层次感。怎么开始:在LazyVStack的每个 item 上加.visualEffect { content, proxy in content.hueRotation(...) },调试 y 坐标除数直到渐变自然。 -
做什么:用 MeshGradient 替换品牌页面的静态背景色。为什么值得做:MeshGradient 控制点可动画,比 CAGradientLayer 灵活,且纯声明式、无需 Core Animation 代码。怎么开始:定义 3x3 MeshGradient,把中心点坐标绑到
@State,用withAnimation(.easeInOut(duration: 2))切换坐标值,观察颜色流动效果。 -
做什么:用 TextRenderer + TextAttribute 做关键信息的逐字入场动画。为什么值得做:普通 opacity 转场所有文字同时出现,没有重点;逐字 spring 动画可以让标题中的关键词有强调感,吸引用户视线。怎么开始:定义
EmphasisAttribute: TextAttribute,在 Text 中用.customAttribute(EmphasisAttribute())标记关键词,renderer 中检查 run 的 attribute 有无来决定走逐字动画还是快速淡入。 -
做什么:给图片交互加 Metal shader 波纹效果。为什么值得做:纯缩放反馈无方向感,波纹从触摸点扩散让用户明确感知到”我点在了这里”。怎么开始:在 metal 文件中写
[[ stitchable ]]Ripple 函数,SwiftUI 端用keyframeAnimator驱动 elapsedTime,通过layerEffect应用 shader。
关联 Session
- What’s new in SwiftUI — iOS 18 SwiftUI 全局更新概览,包含 MeshGradient 等新 API 的首次亮相
- Enhance your UI animations and transitions — 深入 zoom transition 在导航和呈现中的用法,与本 Session 的自定义 Transition 互补
- Create custom hover effects in visionOS — visionOS 上的 hover effect 自定义,同样基于 SwiftUI 视觉效果体系
- SwiftUI essentials — SwiftUI 基础概念回顾,适合先补课再看本 Session 的高级效果
- What’s new in SF Symbols 6 — SF Symbols 新增动画能力,可与 TextRenderer 配合做图标+文字联动效果
评论
GitHub Issues · utterances