swiftui-graphics
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI Graphics
SwiftUI 图形
Advanced SwiftUI visuals: Metal shaders, visual effects, Liquid Glass, Canvas. Loaded for advanced thesis (shaders, holographic, liquid-glass, distortion). Foundation:covers the basics. Concise rules here. Deep-dives in../swiftui-motion/SKILL.md.references/
高级SwiftUI视觉效果:Metal着色器、视觉效果、Liquid Glass、Canvas。 为高级主题设计(着色器、全息、液态玻璃、畸变)。 基础内容:涵盖基础知识。 此处为简明规则,深入内容见../swiftui-motion/SKILL.md。references/
Decision Tree: Which API?
决策树:选择哪个API?
| Need | API |
|---|---|
| Pixel-level color manipulation | |
| Pixel position / distortion | |
| Full layer with overlay (mix shader + bg) | |
| View modifier with geometry context | |
| Custom drawing (paths, gradients) | |
| iOS 26+ glassmorphism | |
| Performance dump | |
Default order of escalation: built-in modifiers ->->.visualEffect-> Metal shader. Reach for shaders only when the effect is per-pixel and animated.Canvas
| 需求 | API |
|---|---|
| 像素级颜色处理 | |
| 像素位置/畸变 | |
| 带叠加层的完整图层(混合着色器+背景) | |
| 带几何上下文的视图修饰符 | |
| 自定义绘图(路径、渐变) | |
| iOS 26+ 毛玻璃效果 | |
| 性能导出 | 启用 |
默认升级顺序: 内置修饰符 ->->.visualEffect-> Metal着色器。仅当效果是逐像素且需要动画时才使用着色器。Canvas
Metal Shaders Intro
Metal着色器简介
SwiftUI binds to Metal Shading Language (MSL) via three modifiers shipped in iOS 17: , , . You author a file in your app target, mark functions with the attribute, and SwiftUI auto-generates the Swift binding via . One library per app target. Shaders run on the GPU at native resolution; arguments are passed as , , , from Swift. iOS 17+ only; for older targets, fall back to gradients, blur, or .
.colorEffect.distortionEffect.layerEffect.metal[[ stitchable ]]ShaderLibrary.<functionName>(...).float.float2.color.imageCanvasThe three slots differ by what data they receive:
- : gets
.colorEffect, returns transformed color. No neighbor sampling.(position, color) - : gets
.distortionEffect, returns a new sample position. Pixels move, colors do not change.(position) - : gets
.layerEffect, returns final color. Can sample anywhere within(position, SwiftUI::Layer layer). Most expensive.maxSampleOffset
SwiftUI通过iOS 17推出的三个修饰符绑定到Metal着色语言(MSL):、、。你可以在应用目标中创建 文件,为函数添加 属性,SwiftUI会通过 自动生成Swift绑定。每个应用目标对应一个库。着色器在GPU上以原生分辨率运行;参数从Swift端以 、、、 类型传递。仅支持iOS 17+;对于旧版本系统,可回退使用渐变、模糊或 。
.colorEffect.distortionEffect.layerEffect.metal[[ stitchable ]]ShaderLibrary.<functionName>(...).float.float2.color.imageCanvas这三个修饰符的区别在于接收的数据不同:
- :接收
.colorEffect,返回转换后的颜色。不支持邻域采样。(position, color) - :接收
.distortionEffect,返回新的采样位置。像素移动,颜色不变。(position) - :接收
.layerEffect,返回最终颜色。可在(position, SwiftUI::Layer layer)范围内任意采样。性能开销最大。maxSampleOffset
Recipe: Ripple .layerEffect
.layerEffect示例:波纹 .layerEffect
.layerEffectTouch 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:
- exposes the function to SwiftUI's runtime.
[[ stitchable ]] - The first two args (,
position) are injected by SwiftUI for anySwiftUI::Layer layer. Your Swift-side args start at index 2..layerEffect - tells SwiftUI how far you may sample beyond the view bounds. Underestimate and you get clipping. Overestimate and you waste GPU.
maxSampleOffset
触摸时产生的波纹效果,沿正弦波位移附近像素。
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);
}工作原理:
- 将函数暴露给SwiftUI运行时。
[[ stitchable ]] - 前两个参数(,
position)由SwiftUI为所有SwiftUI::Layer layer注入。Swift端的参数从索引2开始。.layerEffect - 告知SwiftUI你可能采样超出视图边界的最大距离。设置过小会导致裁剪,过大会浪费GPU资源。
maxSampleOffset
Recipe: Holographic .colorEffect
.colorEffect示例:全息 .colorEffect
.colorEffectOil-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. The version above only tints, which reads as CRT at small scale..layerEffect
复古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采样位置),请改用。上述版本仅进行着色,在小尺寸下可呈现CRT效果。.layerEffect
.visualEffect
(iOS 17+)
.visualEffect.visualEffect
(iOS 17+)
.visualEffectModifier that exposes the view's so you can react to its frame in any coordinate space without boilerplate.
GeometryProxyGeometryReaderswift
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))
is purely visual: the geometry it returns is read-only and the modifier cannot trigger state updates. Don't try to write to.visualEffectfrom inside.@State
该修饰符暴露视图的 ,让你无需 模板代码即可响应其在任意坐标空间中的帧。
GeometryProxyGeometryReaderswift
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: usefor static glass. For morphing transitions, fall back to.background(.ultraThinMaterial)on a material-backed view (seematchedGeometryEffect).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.mdCanvas (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 so the closure re-runs at frame rate:
TimelineView(.animation)swift
TimelineView(.animation) { timeline in
Canvas { context, size in
let t = timeline.date.timeIntervalSinceReferenceDate
// draw using t as time
}
}redraws at the screen refresh rate. UseTimelineView(.animation)for slower animations to save power..animation(minimumInterval: 0.1)
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.mdPerformance Considerations
性能考量
| Effect | Cost | Notes |
|---|---|---|
| low | Runs per pixel, simple math, no neighbor access |
| medium | Sampling cost, branch-free is critical |
| high | Full layer access, can sample anywhere |
| varies | Depends on draw count and complexity |
| medium-high | GPU heavy, fine on modern devices |
Rules:
- Avoid stacking >1 on the same view -- each is a full render pass.
.layerEffect - Precompute static parts (gradients, paths) outside the animated subtree.
- Profile with Instruments GPU Frame Capture and Metal System Trace before optimizing blindly.
| 效果 | 开销 | 说明 |
|---|---|---|
| 低 | 逐像素运行,运算简单,无邻域访问 |
| 中 | 采样开销,无分支代码至关重要 |
| 高 | 完整图层访问,可任意采样 |
带TimelineView的 | 可变 | 取决于绘制次数和复杂度 |
| 中-高 | GPU消耗大,在现代设备上表现良好 |
规则:
- 避免在同一视图上叠加超过1个 ——每个都是完整的渲染通道。
.layerEffect - 在动画子树外部预计算静态部分(渐变、路径)。
- 在盲目优化前,使用Instruments的GPU帧捕获和Metal系统跟踪进行性能分析。
Anti-Patterns
反模式
1. Stacked .colorEffect
modifiers
.colorEffect1. 堆叠 .colorEffect
修饰符
.colorEffectEach 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 or , you redraw on input you never intended.
TimelineViewEquatableViewswift
// 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都会重新运行。如果没有 或 ,会在非预期的输入时重绘。
TimelineViewEquatableViewswift
// 糟糕——父视图重绘时也会重绘
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
.glassEffect4. 到处使用 .glassEffect
.glassEffectLiquid 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
快速参考
| Need | Load |
|---|---|
| Metal recipes deep dive | |
| Liquid Glass patterns + iOS 26 specifics | |
| Canvas drawing patterns | |
| Base animations | |
| Foundation | |
| Mobile UX (iOS) | |
| Desktop UX (macOS) | |
| 需求 | 资源 |
|---|---|
| Metal示例深入 | |
| Liquid Glass模式 + iOS 26细节 | |
| Canvas绘图模式 | |
| 基础动画 | |
| 基础原理 | |
| 移动UX(iOS) | |
| 桌面UX(macOS) | |
Sources
参考来源
- twostraws/Inferno - reference Metal shaders for SwiftUI
- Treata11/iShader
- jamesrochabrun/ShaderKit
- raphaelsalaja/metallurgy
- eleev/swiftui-new-metal-shaders
- Apple Metal Shading Language Spec
- Apple Shader documentation