关于 swiftui-graphics
面向 Claude Code、claude.ai 和 Cowork 的 SwiftUI 高级视觉技能,涵盖 Metal 着色器、Canvas、视觉效果与 Liquid Glass,帮助开发者选择渲染 API,并说明性能和回退方案。
主要功能
- 01Metal 着色器效果
- 02Canvas 绘制与动画
- 03结合几何信息的视觉效果
- 04Liquid Glass 模式
- 05渲染性能指导
适用场景
- 01创建像素级颜色或扭曲效果
- 02绘制动态矢量视觉
- 03应用响应滚动的卡片效果
- 04为支持的界面添加玻璃质感表面
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ athevon/ genjutsu --skill swiftui-graphics安装后在 Claude Code 中输入/swiftui-graphics确认已加载。
详细介绍
版本敏感。 以下每个 API 名称、SDK 门槛和浏览器支持声明均已于 2026-09-08 根据一手资料核实。核实了哪些内容以及核实时间,详见
_jutsu/VERSIONS.md。如果该日期已久远,请在依据版本号采取行动前重新核实。
SwiftUI 图形
高级 SwiftUI 视觉效果:Metal 着色器、视觉效果、Liquid Glass、Canvas。 用于高级主题(着色器、全息效果、液态玻璃、扭曲)。 基础内容:
../swiftui-motion/SKILL.md涵盖基础知识。 此处提供简明规则。深入内容见references/。
决策树:该用哪个 API?
| 需求 | 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 着色器简介
SwiftUI 通过 iOS 17 随附的三个修饰符与 Metal 着色语言(MSL)绑定:.colorEffect、.distortionEffect、.layerEffect。你可以在应用目标中编写 .metal 文件,为函数添加 [[ stitchable ]] 属性,SwiftUI 会通过 ShaderLibrary.<functionName>(...) 自动生成 Swift 绑定。每个应用目标对应一个库。着色器在 GPU 上以原生分辨率运行;参数从 Swift 侧以 .float、.float2、.color、.image 传入。可用性:iOS 17.0+、iPadOS 17.0+、Mac Catalyst 17.0+、macOS 14.0+、tvOS 17.0+、visionOS 1.0+,不支持 watchOS。低于这些版本时,请改用渐变、模糊或 Canvas。
三个插槽接收的数据各不相同:
.colorEffect:接收(position, color),返回变换后的颜色。不能对相邻像素采样。.distortionEffect:接收(position),返回新的采样位置。像素会移动,但颜色不会改变。.layerEffect:接收(position, SwiftUI::Layer layer),返回最终颜色。可以在maxSampleOffset范围内任意采样。开销最大。
示例:波纹 .layerEffect
触摸波纹,沿正弦波置换附近的像素。
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
}
}
}
}
// 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 资源。
示例:全息 .colorEffect
由时间或滚动偏移驱动的油膜彩虹闪烁效果。保留亮度,因此暗部仍保持暗色。
// 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);
}
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 的偏移分别是 2pi/3 和 4pi/3——它们会将三个正弦波分散到 RGB 相位中。请保留这些值。
示例:CRT 扫描线
复古 CRT 效果:扫描线、闪烁和轻微色差。
// 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);
}
.colorEffect(ShaderLibrary.crt(.float(elapsed)))
如需真实的色差(偏移 R/B 采样位置),请升级为
.layerEffect。上面的版本仅添加色调,在小尺寸下看起来像 CRT。
.visualEffect(iOS 17+)
此修饰符会公开视图的 GeometryProxy,让你无需编写 GeometryReader 样板代码,即可根据视图在任意坐标空间中的框架作出响应。
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)
系统级玻璃拟态,具有自适应深度和形变过渡效果。内置于操作系统中,并在系统层面进行了优化。
struct HeroCard: View {
// @Namespace is a property wrapper - it has to live inside the View, not at file scope.
@Namespace private var glassNS
var body: some View {
if #available(iOS 26.0, macOS 26.0, *) {
// glassEffectID only does anything inside a GlassEffectContainer.
GlassEffectContainer(spacing: 20.0) {
Label("Hero", systemImage: "sparkles")
.font(.title2)
.padding()
// Default shape is a Capsule (DefaultGlassEffectShape); pass `in:` for anything else.
.glassEffect(.regular, in: .rect(cornerRadius: 24))
.glassEffectID("hero", in: glassNS)
}
} else {
Label("Hero", systemImage: "sparkles")
.font(.title2)
.padding()
.background(.ultraThinMaterial, in: .rect(cornerRadius: 24))
}
}
}
对于应作为一个整体形变的分组表面(例如,悬停时拆分为多个独立胶囊形状的标签栏),请将它们包装在容器中:
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 原生绘图)
矢量绘图 API。路径、渐变、文本、混合模式——无需离开 SwiftUI。
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) 中,使闭包以帧率重新运行:
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。
性能注意事项
| 效果 | 开销 | 说明 |
|---|---|---|
.colorEffect | 低 | 逐像素运行,进行简单计算,不访问相邻像素 |
.distortionEffect | 中 | 有采样开销;避免分支至关重要 |
.layerEffect | 高 | 可访问整个图层,并在任意位置采样 |
带有 TimelineView 的 Canvas | 不定 | 取决于绘制次数和复杂度 |
.glassEffect | 中高 | GPU 开销大,在现代设备上表现良好 |
规则:
- 避免在同一个视图上叠加超过 1 个
.layerEffect——每个都会触发一次完整的渲染通道。 - 将静态部分(渐变、路径)预先计算,并放在动画子树之外。
- 不要盲目优化;请先使用 Instruments 中的 GPU Frame Capture 和 Metal System Trace 进行性能分析。
反模式
1. 叠加 .colorEffect 修饰符
每个修饰符都会触发单独的渲染通道。
// 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)))
2. 每次状态变化都重绘 Canvas
任何祖先视图的状态变化都会导致 Canvas 重新运行。若没有 TimelineView 或 EquatableView,就会在不必要的输入变化时重绘。
// 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) }
}
3. 在着色器中硬编码颜色值
更改颜色时必须重新编译。请改为传入颜色。
// 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);
}
.colorEffect(ShaderLibrary.tinted(.color(themeAccent)))
4. 到处使用 .glassEffect
过度使用 Liquid Glass 会带来高开销和视觉杂乱。请仅用于主视觉或界面装饰表面。
// 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)
}
快速参考
| 需求 | 加载 |
|---|---|
| 深入了解 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 |