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
- Very specific: line 298
Avoid stacking >1 .layerEffect on the same view -- each is a full render pass.
≈ “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.
Files
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
Trending
More talk lately.thin dataabout the whole repository
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.
Other versions3 projects use a version of this.
Word for word · 2
- Jwuthri/Tracely-ai★ 1.5k100% same
- davidanthonyn/claude-skills★ 0100% same
Edited · 1
- AThevon/genjutsu★ 71790% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository43 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- canvas-generativeSkill
- castSkill
- compose-graphicsSkill
- compose-motionSkill
- compose-multiplatformSkill
- css-nativeSkill
- design-auditSkill
- desktop-principlesSkill
- framer-motionSkill
- gsap-coreSkill
- gsap-frameworksSkill
- gsap-performanceSkill
- gsap-pluginsSkill
- gsap-reactSkill
- gsap-scrolltriggerSkill
- gsapSkill
- gsap-timelineSkill
- gsap-utilsSkill
- mobile-principlesSkill
- motion-designSkill
- motion-principlesSkill
- paintSkill
- swiftui-motionSkill
- threejs-r3fSkill
- ui-ux-pro-maxSkill
- ai-observability-langchain-pythonSkill
- competitive-landscapeSkill
- competitor-analysisSkill
- deslopSkill
- keyword-clusteringSkill
- keyword-researchSkill
- link-prospectingSkill
- maintain-greptile-rulesSkill
- merge-readySkill
- openseo-release-notesSkill
- openseo-review-web-contentSkill
- papercutsSkill
- seo-auditSkill
- seo-coachSkill
- seo-project-setupSkill
- simple-issue-descriptionSkill
- tracelySkill
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
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.

