agentleFS
jwuthri-cloudjwuthri-cloud/Tracely-ai/.agents/skills/swiftui-graphics/SKILL.md

swiftui-graphics

Skill1.2k starsChanged 21 days agoMITView on GitHub

Advanced SwiftUI visuals - Metal shaders (.colorEffect, .layerEffect, .distortionEffect), .visualEffect, Liquid Glass (iOS 26), Canvas, holographic and CRT effects.

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Avoid stacking >1 .layerEffect on the same view -- each is a full render pass.

    line 298

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.md covers the basics. Concise rules here. Deep-dives in references/.


Decision Tree: Which 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 dumpCanvas 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 within maxSampleOffset. 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.
  • maxSampleOffset tells 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)))

.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.


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 to matchedGeometryEffect on a material-backed view (see swiftui-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

EffectCostNotes
.colorEffectlowRuns per pixel, simple math, no neighbor access
.distortionEffectmediumSampling cost, branch-free is critical
.layerEffecthighFull layer access, can sample anywhere
Canvas with TimelineViewvariesDepends on draw count and complexity
.glassEffectmedium-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.

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

NeedLoad
Metal recipes deep divereferences/metal-recipes.md
Liquid Glass patterns + iOS 26 specificsreferences/liquid-glass-deep.md
Canvas drawing patternsreferences/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

Download now (ZIP)

Trending

More talk lately.thin dataabout the whole repository

Dec 2025: 0 mentionsJan 2026: 0 mentionsFeb 2026: 0 mentionsMar 2026: 0 mentionsApr 2026: 0 mentionsMay 2026: 0 mentionsJun 2026: 0 mentionsJul 2026: 0 mentionsAug 2026: 0 mentionsSep 2026: 0 mentionsOct 2026: 1 mentionNov 2026: 0 mentions
Dec 2025Jun 2026Nov 2026
Mentions a month of the jwuthri-cloud/Tracely-ai repository that our weekly crawl found: only 1 in all, so this is a hint, not a trend. There is no adoption data for this file yet.

Who's talking about jwuthri-cloud/Tracely-ai

The biggest 1 of 1 mention linking to the repository that our crawl found. Each line is their own headline.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.