Twelve Metal shaders applied to ordinary SwiftUI views with .colorEffect, .distortionEffect and .layerEffect. Each one has a live preview, interactive parameters, touch input, and its Metal source viewable in the app.
![]() Ripple |
![]() Liquid |
![]() Aurora |
![]() Tokamak Plasma |
![]() Water Caustics |
![]() Chromatic Aberration |
![]() CRT Monitor |
![]() Glitch |
Pixelate |
![]() Halftone |
![]() Bokeh Glow |
![]() Noise Dissolve |
Recorded from the iOS simulator with scripts/capture-gifs.sh. Drag effects drift on their own until you touch them, so they never sit still.
| Effect | Modifier | Interaction | What it does |
|---|---|---|---|
| Ripple | .layerEffect |
tap | A decaying wave radiates from the tap and lights up the crests |
| Liquid | .distortionEffect |
drag | Domain-warped noise, plus a lens that bulges or pinches under your finger |
| Aurora | .colorEffect |
Generative northern-lights curtains and stars, screened over the content | |
| Tokamak Plasma | .colorEffect |
D-shaped fusion plasma (Miller elongation/triangularity) with a hot core, H-mode edge and turbulent filaments | |
| Water Caustics | .layerEffect |
Refraction through moving water plus a tileable caustic light pattern | |
| Chromatic Aberration | .layerEffect |
drag | Red and blue split from green around a lens center |
| CRT Monitor | .layerEffect |
Barrel-curved glass, scanlines, an RGB aperture grille, a vignette and a rolling refresh band | |
| Glitch | .layerEffect |
Quantized-time noise shifts slices, splits channels and inverts blocks | |
| Pixelate | .layerEffect |
Mosaic tiles that blend from squares to LED dots | |
| Halftone | .layerEffect |
Print dots on a rotated grid, in mono ink or source color | |
| Bokeh Glow | .layerEffect |
Golden-angle spiral sampling blooms highlights into lens-like discs | |
| Noise Dissolve | .colorEffect |
Burns away along a noise field with a glowing ember edge |
Every effect can be applied to a SwiftUI view or to a bundled sample photo. The photo is generated from code by scripts/generate-assets.swift, as is the app icon, so the repo contains no third-party images.
Since iOS 17, SwiftUI can run a Metal function on every pixel of a view. You write a [[ stitchable ]] function in a .metal file in your target. Xcode compiles it into default.metallib, and ShaderLibrary looks it up by name.
#include <metal_stdlib>
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;
[[ stitchable ]]
half4 ripple(float2 position, SwiftUI::Layer layer,
float2 touch, float time, float amplitude) {
float2 delta = position - touch;
float wave = amplitude * sin(length(delta) * 0.08 - time * 12.0);
return layer.sample(position + normalize(delta + 0.001) * wave);
}TimelineView(.animation) { context in
let time = context.date.timeIntervalSince(start)
Image("SamplePhoto")
.layerEffect(
ShaderLibrary.ripple(.float2(touch), .float(time), .float(12)),
maxSampleOffset: CGSize(width: 12, height: 12)
)
}Each modifier gets a different amount of power:
.colorEffecttakes(float2 position, half4 color, ...)and returns a new color. It only sees its own pixel, which suits tints, generators and masks..distortionEffecttakes(float2 position, ...)and returns where to sample from. It never sees color, which suits warps and lenses..layerEffecttakes(float2 position, SwiftUI::Layer layer, ...)and canlayer.sample(_:)anywhere. This is needed for blurs, bloom, pixelation and channel splits.maxSampleOffsettells SwiftUI how far outside the view you might read.
Arguments after the fixed ones map in order to Shader.Argument values such as .float, .float2, .color, .image or .floatArray. TimelineView(.animation) provides a clock to animate with.
All shaders share one uniform layout, size, time, touch, touchAge, <parameters...>, documented in Common.h. Because of that, a single generic code path builds any effect from its catalog entry:
Shader(function: ShaderFunction(library: .default, name: effect.functionName),
arguments: [.float2(size), .float(time), .float2(touch), .float(touchAge)]
+ parameters.map { .float($0) })ShaderGallery/
App/ ShaderGalleryApp
Model/ ShaderEffect, ShaderParameter, EffectCatalog, EffectSettings (@Observable),
PlaybackClock, ShaderUniforms, GalleryModel, ShaderSourceLoader
Views/ GalleryView, EffectCard, EffectDetailView, EffectCanvas, ParameterSlider,
ShaderEffectModifier, ShaderSourceView, SubjectView
Shaders/ Common.h + one .metal file per effect
ShaderGalleryTests/ Swift Testing suites
scripts/ generate-assets.swift, capture-screenshots.sh, capture-gifs.sh
- Observation.
EffectSettingsis@Observableand shared by the gallery card and the detail screen, so tweaks show up in both. - Pausable time.
PlaybackClockturnsTimelineViewdates into shader time that stops while paused. - Source viewer. A build phase copies the
.metalfiles into the bundle, so the in-app source always matches what was compiled. - Accessibility. With Reduce Motion on, gallery cards show a still frame and detail screens open paused; you can still press Play. Cards and canvases have VoiceOver labels and values. Touch-driven effects offer custom actions ("Move center left", "Start ripple at center") so VoiceOver users can drive them without dragging.
- No third-party dependencies.
- Xcode 26 or later (developed with Xcode 27 and the iOS 27 SDK)
- iOS / iPadOS 18.0+
- visionOS 2.0+ (builds natively; not yet tested on device)
- Apple silicon Macs can run it as "Designed for iPad"
# Build
xcodebuild -project ShaderGallery.xcodeproj -scheme ShaderGallery \
-destination 'id=<SIMULATOR_UDID>' build
# Test
xcodebuild test -project ShaderGallery.xcodeproj -scheme ShaderGallery \
-destination 'id=<SIMULATOR_UDID>'The tests check parameter clamping and snapping, the pause and resume math of the playback clock, touch handling, the uniform layout, and that every catalog entry has bundled source and a function in the compiled default.metallib.
To run on a device, choose your own team under Signing & Capabilities.
scripts/capture-screenshots.sh <SIMULATOR_UDID> # default set
scripts/capture-screenshots.sh <SIMULATOR_UDID> "ripple:photo glitch"
scripts/capture-gifs.sh <SIMULATOR_UDID> # all twelve, needs ffmpeg
scripts/capture-gifs.sh <SIMULATOR_UDID> tokamak:sceneThe app accepts launch arguments that open a screen directly: -openEffect <id>, -subject scene|photo, -showSource YES.
MIT. See LICENSE.
Matthew Waller















