Swiftui graphics
Creative coding skills for Claude: animations, 3D, design systems, motion principles
npx -y skills add AThevon/genjutsu --skill swiftui-graphicsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
Advanced SwiftUI visuals - Metal shaders (.colorEffect, .layerEffect, .distortionEffect), .visualEffect, Liquid Glass (iOS 26), Canvas, holographic and CRT effects.
SKILL.md
12.9 KB, as published. Nobody here has run it
SwiftUI Graphics
Advanced SwiftUI visuals: Metal shaders, visual effects, Liquid Glass, Canvas. Loaded for advanced thesis (shaders, holographic, liquid-glass, distortion). Foundation:
../swiftui-motion/SKILL.mdcovers the basics. Concise rules here. Deep-dives inreferences/.
Decision Tree: Which API?
| Need | API |
|---|---|
| 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.
Metal Shaders Intro
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 withinmaxSampleOffset. Most expensive.
Recipe: Ripple .layerEffect
Touch ripple that displaces nearby pixels along a sine wave.
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);
}
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. maxSampleOffsettells SwiftUI how far you may sample beyond the view bounds. Underestimate and you get clipping. Overestimate and you waste GPU.
Recipe: Holographic .colorEffect
Oil-slick rainbow shimmer driven by time or scroll offset. Preserves luminance so dark regions stay dark.
// 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))
)
}
}
}
The 2.094 and 4.188 offsets are 2pi/3 and 4pi/3 -- they spread the three sine waves to RGB phases. Keep them.
Recipe: CRT Scanlines
Vintage CRT effect: scanlines, flicker, subtle chromatic aberration.
// 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)))
For real chromatic aberration (shifted R/B sample positions), promote to
.layerEffect. The version above only tints, which reads as CRT at small scale.
.visualEffect (iOS 17+)
Modifier that exposes the view's GeometryProxy so you can react to its frame in any coordinate space without GeometryReader boilerplate.
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)))
.visualEffectis purely visual: the geometry it returns is read-only and the modifier cannot trigger state updates. Don't try to write to@Statefrom inside.
Liquid Glass (iOS 26)
System glassmorphism with adaptive depth and morphing transitions. Built into the OS, optimized at the system level.
@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:
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 tomatchedGeometryEffecton a material-backed view (seeswiftui-motion).
Deep-dive: references/liquid-glass-deep.md.
Canvas (SwiftUI Native Drawing)
Vector drawing API. Paths, gradients, text, blend modes -- without leaving 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)))
)
}
}
}
}
To animate, wrap in TimelineView(.animation) so the closure re-runs at frame rate:
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.
Performance Considerations
| Effect | Cost | Notes |
|---|---|---|
.colorEffect | low | Runs per pixel, simple math, no neighbor access |
.distortionEffect | medium | Sampling cost, branch-free is critical |
.layerEffect | high | Full layer access, can sample anywhere |
Canvas with TimelineView | varies | Depends on draw count and complexity |
.glassEffect | medium-high | GPU heavy, fine on modern devices |
Rules:
- Avoid stacking >1
.layerEffecton the same view -- each is a full render pass. - Precompute static parts (gradients, paths) outside the animated subtree.
- Profile with Instruments GPU Frame Capture and Metal System Trace before optimizing blindly.
Anti-Patterns
1. Stacked .colorEffect modifiers
Each modifier is a separate render pass.
// 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 redrawn on every state change
Canvas re-runs when any ancestor state changes. Without TimelineView or EquatableView, you redraw on input you never intended.
// 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. Hardcoded color values inside shader
Forces a recompile to change color. Pass them through.
// 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 everywhere
Liquid Glass is expensive and visually noisy when overused. Reserve for hero / chrome surfaces.
// 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)
}
Quick Reference
| Need | Load |
|---|---|
| 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 |
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