WWDC Quick Look 💓 By SwiftGGTeam
Create custom visual effects with SwiftUI

Create custom visual effects with SwiftUI

观看原视频

Highlight

SwiftUI 在 iOS 18 新增了 MeshGradient、TextRenderer、自定义 Transition 协议和 Metal shader 集成,让视觉效果的实现从”只能用内置 modifier”变成”像素级自定义”。


核心内容

做 SwiftUI 应用时,视觉动效常常卡在两件事上:要么内置 modifier 不够用,比如 scrollTransition 只能做旋转缩放,想做色相偏移就得手写 GeometryReader;要么想做文字逐字动画,但 Text 视图是不可拆分的黑盒,拿不到单个 glyph 的位置。

这回 Apple 给了五个新工具来补齐这些缺口。scrollTransitionvisualEffect 两个 modifier 覆盖了滚动场景下基于位置和基于几何信息的两种视觉调整方式。MeshGradient 用一组带颜色的控制点在网格上做插值,比线性/径向渐变灵活得多,控制点位置还能动画。自定义 Transition 协议让你用 TransitionPhase 区分 willAppear 和 didDisappear,写出有方向感的转场。TextRenderer 协议把 Text.Layout 的 lines、runs、runSlices 全部暴露出来,配合 TextAttribute 标记关键文字,可以逐字做 spring 动画。最后,Metal shader 通过 ShaderLibrarylayerEffect 直接集成到 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 的闭包参数 proxyGeometryProxy,可读取视图的 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 == .willAppearphase == .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

评论

GitHub Issues · utterances