swiftui-graphics

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Graphics

SwiftUI 图形

Advanced SwiftUI visuals: Metal shaders, visual effects, Liquid Glass, Canvas. Loaded for advanced thesis (shaders, holographic, liquid-glass, distortion). Foundation:
../swiftui-motion/SKILL.md
covers the basics. Concise rules here. Deep-dives in
references/
.

高级SwiftUI视觉效果:Metal着色器、视觉效果、Liquid Glass、Canvas。 为高级主题设计(着色器、全息、液态玻璃、畸变)。 基础内容:
../swiftui-motion/SKILL.md
涵盖基础知识。 此处为简明规则,深入内容见
references/

Decision Tree: Which API?

决策树:选择哪个API?

NeedAPI
Pixel-level color manipulation
.colorEffect(ShaderLibrary....)
Pixel position / distortion
.distortionEffect(ShaderLibrary....)
Full layer with overlay (mix shader + bg)
.layerEffect(ShaderLibrary....)
View modifier with geometry context
.visualEffect { content, geometry in }
Custom drawing (paths, gradients)
Canvas { context, size in }
iOS 26+ glassmorphism
.glassEffect()
/
GlassEffectContainer
Performance dump
Canvas
with
.opaque(true)
then export
Default order of escalation: built-in modifiers ->
.visualEffect
->
Canvas
-> Metal shader. Reach for shaders only when the effect is per-pixel and animated.

需求API
像素级颜色处理
.colorEffect(ShaderLibrary....)
像素位置/畸变
.distortionEffect(ShaderLibrary....)
带叠加层的完整图层(混合着色器+背景)
.layerEffect(ShaderLibrary....)
带几何上下文的视图修饰符
.visualEffect { content, geometry in }
自定义绘图(路径、渐变)
Canvas { context, size in }
iOS 26+ 毛玻璃效果
.glassEffect()
/
GlassEffectContainer
性能导出启用
.opaque(true)
Canvas
导出
默认升级顺序: 内置修饰符 ->
.visualEffect
->
Canvas
-> Metal着色器。仅当效果是逐像素且需要动画时才使用着色器。

Metal Shaders Intro

Metal着色器简介

SwiftUI binds to Metal Shading Language (MSL) via three modifiers shipped in iOS 17:
.colorEffect
,
.distortionEffect
,
.layerEffect
. You author a
.metal
file in your app target, mark functions with the
[[ stitchable ]]
attribute, and SwiftUI auto-generates the Swift binding via
ShaderLibrary.<functionName>(...)
. One library per app target. Shaders run on the GPU at native resolution; arguments are passed as
.float
,
.float2
,
.color
,
.image
from Swift. iOS 17+ only; for older targets, fall back to gradients, blur, or
Canvas
.
The three slots differ by what data they receive:
  • .colorEffect
    : gets
    (position, color)
    , returns transformed color. No neighbor sampling.
  • .distortionEffect
    : gets
    (position)
    , returns a new sample position. Pixels move, colors do not change.
  • .layerEffect
    : gets
    (position, SwiftUI::Layer layer)
    , returns final color. Can sample anywhere within
    maxSampleOffset
    . Most expensive.

SwiftUI通过iOS 17推出的三个修饰符绑定到Metal着色语言(MSL):
.colorEffect
.distortionEffect
.layerEffect
。你可以在应用目标中创建
.metal
文件,为函数添加
[[ stitchable ]]
属性,SwiftUI会通过
ShaderLibrary.<functionName>(...)
自动生成Swift绑定。每个应用目标对应一个库。着色器在GPU上以原生分辨率运行;参数从Swift端以
.float
.float2
.color
.image
类型传递。仅支持iOS 17+;对于旧版本系统,可回退使用渐变、模糊或
Canvas
这三个修饰符的区别在于接收的数据不同:
  • .colorEffect
    :接收
    (position, color)
    ,返回转换后的颜色。不支持邻域采样。
  • .distortionEffect
    :接收
    (position)
    ,返回新的采样位置。像素移动,颜色不变。
  • .layerEffect
    :接收
    (position, SwiftUI::Layer layer)
    ,返回最终颜色。可在
    maxSampleOffset
    范围内任意采样。性能开销最大。

Recipe: Ripple
.layerEffect

示例:波纹
.layerEffect

Touch ripple that displaces nearby pixels along a sine wave.
swift
struct RippleView: View {
    @State var rippleOrigin: CGPoint = .zero
    @State var rippleTime: Float = 0

    var body: some View {
        Image("photo")
            .resizable()
            .scaledToFit()
            .layerEffect(
                ShaderLibrary.ripple(
                    .float2(Float(rippleOrigin.x), Float(rippleOrigin.y)),
                    .float(rippleTime),
                    .float(0.05) // amplitude
                ),
                maxSampleOffset: CGSize(width: 50, height: 50)
            )
            .onTapGesture { location in
                rippleOrigin = location
                rippleTime = 0
                withAnimation(.linear(duration: 1.2)) {
                    rippleTime = 1.2
                }
            }
    }
}
metal
// Ripple.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 ripple(float2 position, SwiftUI::Layer layer,
             float2 origin, float time, float amp) {
    float distance = length(position - origin);
    float wave = sin(distance * 0.05 - time * 8.0) * amp;
    float2 dir = normalize(position - origin);
    float falloff = 1.0 / max(distance, 1.0);
    float2 displaced = position + dir * wave * falloff * 50.0;
    return layer.sample(displaced);
}
Why this works:
  • [[ stitchable ]]
    exposes the function to SwiftUI's runtime.
  • The first two args (
    position
    ,
    SwiftUI::Layer layer
    ) are injected by SwiftUI for any
    .layerEffect
    . Your Swift-side args start at index 2.
  • maxSampleOffset
    tells SwiftUI how far you may sample beyond the view bounds. Underestimate and you get clipping. Overestimate and you waste GPU.

触摸时产生的波纹效果,沿正弦波位移附近像素。
swift
struct RippleView: View {
    @State var rippleOrigin: CGPoint = .zero
    @State var rippleTime: Float = 0

    var body: some View {
        Image("photo")
            .resizable()
            .scaledToFit()
            .layerEffect(
                ShaderLibrary.ripple(
                    .float2(Float(rippleOrigin.x), Float(rippleOrigin.y)),
                    .float(rippleTime),
                    .float(0.05) // amplitude
                ),
                maxSampleOffset: CGSize(width: 50, height: 50)
            )
            .onTapGesture { location in
                rippleOrigin = location
                rippleTime = 0
                withAnimation(.linear(duration: 1.2)) {
                    rippleTime = 1.2
                }
            }
    }
}
metal
// Ripple.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 ripple(float2 position, SwiftUI::Layer layer,
             float2 origin, float time, float amp) {
    float distance = length(position - origin);
    float wave = sin(distance * 0.05 - time * 8.0) * amp;
    float2 dir = normalize(position - origin);
    float falloff = 1.0 / max(distance, 1.0);
    float2 displaced = position + dir * wave * falloff * 50.0;
    return layer.sample(displaced);
}
工作原理:
  • [[ stitchable ]]
    将函数暴露给SwiftUI运行时。
  • 前两个参数(
    position
    ,
    SwiftUI::Layer layer
    )由SwiftUI为所有
    .layerEffect
    注入。Swift端的参数从索引2开始。
  • maxSampleOffset
    告知SwiftUI你可能采样超出视图边界的最大距离。设置过小会导致裁剪,过大会浪费GPU资源。

Recipe: Holographic
.colorEffect

示例:全息
.colorEffect

Oil-slick rainbow shimmer driven by time or scroll offset. Preserves luminance so dark regions stay dark.
metal
// Holographic.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 holographic(float2 position, half4 color, float time) {
    float n = position.x * 0.01 + position.y * 0.005 + time * 0.3;
    half3 rainbow = half3(
        sin(n * 2.0) * 0.5 + 0.5,
        sin(n * 2.0 + 2.094) * 0.5 + 0.5,
        sin(n * 2.0 + 4.188) * 0.5 + 0.5
    );
    half luminance = dot(color.rgb, half3(0.299, 0.587, 0.114));
    return half4(mix(color.rgb, rainbow * luminance * 2.0, 0.5), color.a);
}
swift
struct HolographicCard: View {
    let startTime = Date()

    var body: some View {
        TimelineView(.animation) { timeline in
            let elapsed = Float(timeline.date.timeIntervalSince(startTime))
            Image("card")
                .resizable()
                .scaledToFit()
                .colorEffect(
                    ShaderLibrary.holographic(.float(elapsed))
                )
        }
    }
}
The 2.094 and 4.188 offsets are 2pi/3 and 4pi/3 -- they spread the three sine waves to RGB phases. Keep them.

由时间或滚动偏移驱动的油膜彩虹闪烁效果。保留亮度,使深色区域保持深色。
metal
// Holographic.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 holographic(float2 position, half4 color, float time) {
    float n = position.x * 0.01 + position.y * 0.005 + time * 0.3;
    half3 rainbow = half3(
        sin(n * 2.0) * 0.5 + 0.5,
        sin(n * 2.0 + 2.094) * 0.5 + 0.5,
        sin(n * 2.0 + 4.188) * 0.5 + 0.5
    );
    half luminance = dot(color.rgb, half3(0.299, 0.587, 0.114));
    return half4(mix(color.rgb, rainbow * luminance * 2.0, 0.5), color.a);
}
swift
struct HolographicCard: View {
    let startTime = Date()

    var body: some View {
        TimelineView(.animation) { timeline in
            let elapsed = Float(timeline.date.timeIntervalSince(startTime))
            Image("card")
                .resizable()
                .scaledToFit()
                .colorEffect(
                    ShaderLibrary.holographic(.float(elapsed))
                )
        }
    }
}
2.094和4.188的偏移量是2π/3和4π/3——它们将三个正弦波分布到RGB相位。请保留这些值。

Recipe: CRT Scanlines

示例:CRT扫描线

Vintage CRT effect: scanlines, flicker, subtle chromatic aberration.
metal
// CRT.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 crt(float2 position, half4 color, float time) {
    float scanline = sin(position.y * 1.5) * 0.04;
    float flicker = sin(time * 60.0) * 0.02;
    half3 result = color.rgb * (1.0 - scanline - flicker);
    // chromatic aberration on R/B channels
    return half4(result.r * 1.05, result.g, result.b * 1.05, color.a);
}
swift
.colorEffect(ShaderLibrary.crt(.float(elapsed)))
For real chromatic aberration (shifted R/B sample positions), promote to
.layerEffect
. The version above only tints, which reads as CRT at small scale.

复古CRT效果:扫描线、闪烁、轻微色差。
metal
// CRT.metal
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;

[[ stitchable ]]
half4 crt(float2 position, half4 color, float time) {
    float scanline = sin(position.y * 1.5) * 0.04;
    float flicker = sin(time * 60.0) * 0.02;
    half3 result = color.rgb * (1.0 - scanline - flicker);
    // chromatic aberration on R/B channels
    return half4(result.r * 1.05, result.g, result.b * 1.05, color.a);
}
swift
.colorEffect(ShaderLibrary.crt(.float(elapsed)))
如需真实的色差(偏移R/B采样位置),请改用
.layerEffect
。上述版本仅进行着色,在小尺寸下可呈现CRT效果。

.visualEffect
(iOS 17+)

.visualEffect
(iOS 17+)

Modifier that exposes the view's
GeometryProxy
so you can react to its frame in any coordinate space without
GeometryReader
boilerplate.
swift
ScrollView {
    LazyVStack(spacing: 16) {
        ForEach(items) { item in
            CardView(item: item)
                .visualEffect { content, proxy in
                    let y = proxy.frame(in: .scrollView).minY
                    let scale = scale(for: y)
                    let opacity = opacity(for: y)
                    return content
                        .scaleEffect(scale)
                        .opacity(opacity)
                }
        }
    }
}

func scale(for y: CGFloat) -> CGFloat {
    let progress = max(0, min(1, y / 600))
    return 0.85 + progress * 0.15
}
Use cases:
  • Parallax cards (move slower than scroll)
  • Scroll-driven scale (cards grow as they enter view)
  • Sticky reveal (clamp position via
    .offset(y: max(0, -y))
    )
.visualEffect
is purely visual: the geometry it returns is read-only and the modifier cannot trigger state updates. Don't try to write to
@State
from inside.

该修饰符暴露视图的
GeometryProxy
,让你无需
GeometryReader
模板代码即可响应其在任意坐标空间中的帧。
swift
ScrollView {
    LazyVStack(spacing: 16) {
        ForEach(items) { item in
            CardView(item: item)
                .visualEffect { content, proxy in
                    let y = proxy.frame(in: .scrollView).minY
                    let scale = scale(for: y)
                    let opacity = opacity(for: y)
                    return content
                        .scaleEffect(scale)
                        .opacity(opacity)
                }
        }
    }
}

func scale(for y: CGFloat) -> CGFloat {
    let progress = max(0, min(1, y / 600))
    return 0.85 + progress * 0.15
}
使用场景:
  • 视差卡片(滚动时移动速度更慢)
  • 滚动驱动缩放(卡片进入视图时放大)
  • 粘性显示(通过
    .offset(y: max(0, -y))
    限制位置)
.visualEffect
仅用于视觉效果:它返回的几何信息是只读的,该修饰符无法触发状态更新。请勿尝试在其内部修改
@State

Liquid Glass (iOS 26)

Liquid Glass(iOS 26)

System glassmorphism with adaptive depth and morphing transitions. Built into the OS, optimized at the system level.
swift
@Namespace var glassNS

struct HeroCard: View {
    var body: some View {
        if #available(iOS 26.0, *) {
            Image("hero")
                .resizable()
                .scaledToFit()
                .glassEffect(.regular)
                .glassEffectID("hero", in: glassNS)
        } else {
            Image("hero")
                .resizable()
                .scaledToFit()
                .background(.ultraThinMaterial)
        }
    }
}
For grouped surfaces that should morph as one (e.g., a tab bar that splits into separate pills on hover), wrap them in a container:
swift
GlassEffectContainer(spacing: 12) {
    ForEach(tabs) { tab in
        TabIcon(tab: tab)
            .glassEffect(.regular)
            .glassEffectID(tab.id, in: glassNS)
    }
}
Pre-iOS 26: use
.background(.ultraThinMaterial)
for static glass. For morphing transitions, fall back to
matchedGeometryEffect
on a material-backed view (see
swiftui-motion
).
Deep-dive:
references/liquid-glass-deep.md
.

系统级毛玻璃效果,支持自适应深度和变形过渡。内置在系统中,经过系统级优化。
swift
@Namespace var glassNS

struct HeroCard: View {
    var body: some View {
        if #available(iOS 26.0, *) {
            Image("hero")
                .resizable()
                .scaledToFit()
                .glassEffect(.regular)
                .glassEffectID("hero", in: glassNS)
        } else {
            Image("hero")
                .resizable()
                .scaledToFit()
                .background(.ultraThinMaterial)
        }
    }
}
对于需要作为整体变形的分组表面(例如悬停时从标签栏拆分为独立按钮),将它们包裹在容器中:
swift
GlassEffectContainer(spacing: 12) {
    ForEach(tabs) { tab in
        TabIcon(tab: tab)
            .glassEffect(.regular)
            .glassEffectID(tab.id, in: glassNS)
    }
}
iOS 26之前:使用
.background(.ultraThinMaterial)
实现静态毛玻璃效果。如需变形过渡,请回退到在材质背景视图上使用
matchedGeometryEffect
(详见
swiftui-motion
)。
深入内容:
references/liquid-glass-deep.md

Canvas (SwiftUI Native Drawing)

Canvas(SwiftUI原生绘图)

Vector drawing API. Paths, gradients, text, blend modes -- without leaving SwiftUI.
swift
struct SparkleField: View {
    var body: some View {
        Canvas { context, size in
            for _ in 0..<20 {
                let x = Double.random(in: 0...size.width)
                let y = Double.random(in: 0...size.height)
                let radius = Double.random(in: 1...4)
                context.fill(
                    Path(ellipseIn: CGRect(x: x, y: y, width: radius, height: radius)),
                    with: .color(.white.opacity(.random(in: 0.3...1.0)))
                )
            }
        }
    }
}
To animate, wrap in
TimelineView(.animation)
so the closure re-runs at frame rate:
swift
TimelineView(.animation) { timeline in
    Canvas { context, size in
        let t = timeline.date.timeIntervalSinceReferenceDate
        // draw using t as time
    }
}
TimelineView(.animation)
redraws at the screen refresh rate. Use
.animation(minimumInterval: 0.1)
for slower animations to save power.
Deep-dive:
references/canvas-swiftui.md
.

矢量绘图API。支持路径、渐变、文本、混合模式——无需离开SwiftUI环境。
swift
struct SparkleField: View {
    var body: some View {
        Canvas { context, size in
            for _ in 0..<20 {
                let x = Double.random(in: 0...size.width)
                let y = Double.random(in: 0...size.height)
                let radius = Double.random(in: 1...4)
                context.fill(
                    Path(ellipseIn: CGRect(x: x, y: y, width: radius, height: radius)),
                    with: .color(.white.opacity(.random(in: 0.3...1.0)))
                )
            }
        }
    }
}
如需动画,将其包裹在
TimelineView(.animation)
中,使闭包以帧速率重新运行:
swift
TimelineView(.animation) { timeline in
    Canvas { context, size in
        let t = timeline.date.timeIntervalSinceReferenceDate
        // draw using t as time
    }
}
TimelineView(.animation)
以屏幕刷新率重绘。对于较慢的动画,使用
.animation(minimumInterval: 0.1)
以节省电量。
深入内容:
references/canvas-swiftui.md

Performance Considerations

性能考量

EffectCostNotes
.colorEffect
lowRuns per pixel, simple math, no neighbor access
.distortionEffect
mediumSampling cost, branch-free is critical
.layerEffect
highFull layer access, can sample anywhere
Canvas
with TimelineView
variesDepends on draw count and complexity
.glassEffect
medium-highGPU heavy, fine on modern devices
Rules:
  1. Avoid stacking >1
    .layerEffect
    on the same view -- each is a full render pass.
  2. Precompute static parts (gradients, paths) outside the animated subtree.
  3. Profile with Instruments GPU Frame Capture and Metal System Trace before optimizing blindly.

效果开销说明
.colorEffect
逐像素运行,运算简单,无邻域访问
.distortionEffect
采样开销,无分支代码至关重要
.layerEffect
完整图层访问,可任意采样
带TimelineView的
Canvas
可变取决于绘制次数和复杂度
.glassEffect
中-高GPU消耗大,在现代设备上表现良好
规则:
  1. 避免在同一视图上叠加超过1个
    .layerEffect
    ——每个都是完整的渲染通道。
  2. 在动画子树外部预计算静态部分(渐变、路径)。
  3. 在盲目优化前,使用Instruments的GPU帧捕获Metal系统跟踪进行性能分析。

Anti-Patterns

反模式

1. Stacked
.colorEffect
modifiers

1. 堆叠
.colorEffect
修饰符

Each modifier is a separate render pass.
swift
// BAD -- 5 GPU passes
Image(...)
    .colorEffect(ShaderLibrary.tint(...))
    .colorEffect(ShaderLibrary.scanlines(...))
    .colorEffect(ShaderLibrary.grain(...))
    .colorEffect(ShaderLibrary.vignette(...))
    .colorEffect(ShaderLibrary.chromatic(...))

// GOOD -- one shader does all the ops in one pass
Image(...)
    .colorEffect(ShaderLibrary.crtCombo(.float(time)))
每个修饰符都是独立的渲染通道。
swift
// 糟糕——5次GPU通道
Image(...)
    .colorEffect(ShaderLibrary.tint(...))
    .colorEffect(ShaderLibrary.scanlines(...))
    .colorEffect(ShaderLibrary.grain(...))
    .colorEffect(ShaderLibrary.vignette(...))
    .colorEffect(ShaderLibrary.chromatic(...))

// 良好——一个着色器在一次通道中完成所有操作
Image(...)
    .colorEffect(ShaderLibrary.crtCombo(.float(time)))

2. Canvas redrawn on every state change

2. 每次状态变化都重绘Canvas

Canvas re-runs when any ancestor state changes. Without
TimelineView
or
EquatableView
, you redraw on input you never intended.
swift
// BAD -- redraws on parent re-render
struct Parent: View {
    @State var unrelated = 0
    var body: some View {
        VStack {
            Button("tick") { unrelated += 1 }
            Canvas { context, size in expensiveDraw(context, size) }
        }
    }
}

// GOOD -- isolate via EquatableView or TimelineView
TimelineView(.animation) { _ in
    Canvas { context, size in expensiveDraw(context, size) }
}
当任何祖先状态变化时,Canvas都会重新运行。如果没有
TimelineView
EquatableView
,会在非预期的输入时重绘。
swift
// 糟糕——父视图重绘时也会重绘
struct Parent: View {
    @State var unrelated = 0
    var body: some View {
        VStack {
            Button("tick") { unrelated += 1 }
            Canvas { context, size in expensiveDraw(context, size) }
        }
    }
}

// 良好——通过EquatableView或TimelineView隔离
TimelineView(.animation) { _ in
    Canvas { context, size in expensiveDraw(context, size) }
}

3. Hardcoded color values inside shader

3. 着色器内部硬编码颜色值

Forces a recompile to change color. Pass them through.
metal
// BAD
half3 tint = half3(1.0, 0.4, 0.2);

// GOOD -- accept color from Swift
[[ stitchable ]]
half4 tinted(float2 pos, half4 color, half4 tint) {
    return half4(color.rgb * tint.rgb, color.a);
}
swift
.colorEffect(ShaderLibrary.tinted(.color(themeAccent)))
修改颜色需要重新编译。应通过参数传递。
metal
// 糟糕
half3 tint = half3(1.0, 0.4, 0.2);

// 良好——从Swift接收颜色参数
[[ stitchable ]]
half4 tinted(float2 pos, half4 color, half4 tint) {
    return half4(color.rgb * tint.rgb, color.a);
}
swift
.colorEffect(ShaderLibrary.tinted(.color(themeAccent)))

4.
.glassEffect
everywhere

4. 到处使用
.glassEffect

Liquid Glass is expensive and visually noisy when overused. Reserve for hero / chrome surfaces.
swift
// BAD -- 30 glass cards in a list
LazyVStack {
    ForEach(items) { item in
        Card(item: item).glassEffect(.regular) // GPU melt
    }
}

// GOOD -- glass on the floating tab bar, opaque cards underneath
ZStack(alignment: .bottom) {
    ScrollView { LazyVStack { ForEach(items) { Card(item: $0) } } }
    TabBar().glassEffect(.regular)
}

Liquid Glass开销大,过度使用会导致视觉杂乱。仅用于核心/导航栏等重要表面。
swift
// 糟糕——列表中有30个毛玻璃卡片
LazyVStack {
    ForEach(items) { item in
        Card(item: item).glassEffect(.regular) // GPU过载
    }
}

// 良好——浮动标签栏使用毛玻璃,下方卡片为不透明
ZStack(alignment: .bottom) {
    ScrollView { LazyVStack { ForEach(items) { Card(item: $0) } } }
    TabBar().glassEffect(.regular)
}

Quick Reference

快速参考

NeedLoad
Metal recipes deep dive
references/metal-recipes.md
Liquid Glass patterns + iOS 26 specifics
references/liquid-glass-deep.md
Canvas drawing patterns
references/canvas-swiftui.md
Base animations
../swiftui-motion/SKILL.md
Foundation
../motion-principles/SKILL.md
Mobile UX (iOS)
../mobile-principles/SKILL.md
Desktop UX (macOS)
../desktop-principles/SKILL.md

需求资源
Metal示例深入
references/metal-recipes.md
Liquid Glass模式 + iOS 26细节
references/liquid-glass-deep.md
Canvas绘图模式
references/canvas-swiftui.md
基础动画
../swiftui-motion/SKILL.md
基础原理
../motion-principles/SKILL.md
移动UX(iOS)
../mobile-principles/SKILL.md
桌面UX(macOS)
../desktop-principles/SKILL.md

Sources

参考来源