Skip to content

Chromatic Glitch Shader

A view modifier that breaks a view up like a bad signal: its colors come apart, bands jump sideways and scan lines cross it, drawn by a Metal shader.

Chromatic Glitch Shader, playing
  • Effects
  • iOS 18.0+
  • advanced
  • Metal shader
Download .swiftDownload .metal
Origin
Original work
Found by
glitch · chromatic aberration · rgb split · shader · metal · scan lines · vhs · distortion · glitch · aberración cromática · separación rgb · shader · metal · líneas de escaneo · vhs · distorsión

Breaks this view up like a bad signal, by a Metal shader: its colors come apart, some bands jump sideways and scan lines cross it. amount goes from 0, untouched, to 1, and animates.

A shader draws what SwiftUI draws itself: a view backed by UIKit, such as a map, shows a placeholder.

The bands change ten times a second. With Reduce Motion on they stand still, and only the colors come apart. It only changes how the view looks, not what VoiceOver reads.

struct ChromaticGlitchShaderDemo: View {
@State private var amount = 0.0
/// Only for this demo: it plays by itself until someone touches it.
@State private var isAutoPlaying = true
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
VStack(spacing: 36) {
VStack(spacing: 14) {
Image(systemName: "antenna.radiowaves.left.and.right")
.font(.system(size: 64, weight: .semibold))
.accessibilityHidden(true)
Text("SIGNAL LOST")
.font(.system(.largeTitle, design: .monospaced, weight: .heavy))
Text("Reconnecting to channel 7")
.font(.system(.subheadline, design: .monospaced))
.opacity(0.7)
}
.foregroundStyle(.white)
.padding(.vertical, 44)
.frame(maxWidth: .infinity)
.background(LinearGradient(colors: [.cyan, .blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing), in: .rect(cornerRadius: 28))
.chromaticGlitch(amount)
VStack(alignment: .leading, spacing: 6) {
Text("Glitch")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.white.opacity(0.7))
Slider(value: $amount, in: 0...1)
.tint(.cyan)
.accessibilityLabel("Glitch")
}
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(red: 0.04, green: 0.04, blue: 0.09).ignoresSafeArea())
.simultaneousGesture(DragGesture(minimumDistance: 0).onChanged { _ in isAutoPlaying = false })
.task {
while isAutoPlaying {
try? await Task.sleep(for: .seconds(0.9))
guard !Task.isCancelled, isAutoPlaying else { return }
withAnimation(reduceMotion ? nil : .easeIn(duration: 0.25)) { amount = 1 }
try? await Task.sleep(for: .seconds(1.1))
guard !Task.isCancelled, isAutoPlaying else { return }
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.6)) { amount = 0 }
}
}
}
}

chromaticGlitch(_:)

ParameterTypeDefault
amountDoubleRequired

Copy both files into your project: the Swift file and the Metal shader it draws with. Or download ChromaticGlitchShader.swift and ChromaticGlitchShader.metal.

ChromaticGlitchShader.swift
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
nonisolated enum ChromaticGlitchShaderMath {
/// The farthest a color channel moves from its place, in points.
static let split = 7.0
/// The farthest a band moves sideways, in points.
static let shift = 26.0
static func clamped(_ amount: Double) -> Double {
guard amount.isFinite else { return 0 }
return min(max(amount, 0), 1)
}
/// How far from a point the shader reads, which SwiftUI has to know beforehand.
static func reach(amount: Double) -> Double {
(split + shift) * clamped(amount)
}
/// The function in `ChromaticGlitchShader.metal`, with its arguments in the order it declares them.
static func shader(amount: Double, time: Double, bounds: CGSize) -> Shader {
ShaderLibrary.ChromaticGlitchShaderSplit(.float(clamped(amount)), .float(time), .float(split), .float(shift), .float2(bounds))
}
}
extension View {
/// Breaks this view up like a bad signal, by a Metal shader: its colors come apart, some
/// bands jump sideways and scan lines cross it. `amount` goes from 0, untouched, to 1, and
/// animates.
///
/// - Important: Copy two files into the same target: `ChromaticGlitchShader.swift` and `ChromaticGlitchShader.metal`.
///
/// A shader draws what SwiftUI draws itself: a view backed by UIKit, such as a map, shows a placeholder.
///
/// The bands change ten times a second. With Reduce Motion on they stand still, and only
/// the colors come apart. It only changes how the view looks, not what VoiceOver reads.
func chromaticGlitch(_ amount: Double) -> some View {
modifier(ChromaticGlitchShaderModifier(amount: amount))
}
}
/// `Animatable`, so the shader gets every step of the change: a shader's arguments do not animate by themselves.
private struct ChromaticGlitchShaderModifier: ViewModifier, Animatable {
var amount: Double
@Environment(\.accessibilityReduceMotion) private var reduceMotion
nonisolated var animatableData: Double {
get { amount }
set { amount = newValue }
}
func body(content: Content) -> some View {
let strength = ChromaticGlitchShaderMath.clamped(amount)
let reach = ChromaticGlitchShaderMath.reach(amount: strength)
// The clock only runs while there is something to move.
TimelineView(.animation(minimumInterval: 1 / 30, paused: reduceMotion || strength == 0)) { context in
let time = reduceMotion ? 0 : context.date.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: 600)
content
// `visualEffect` gives the shader the size of the view, which it needs at the edges.
.visualEffect { view, proxy in
view.layerEffect(
ChromaticGlitchShaderMath.shader(amount: strength, time: time, bounds: proxy.size),
maxSampleOffset: CGSize(width: reach, height: 0),
isEnabled: strength > 0
)
}
}
// The first use of a shader waits for it to compile. Doing that now keeps the first frames smooth.
.task {
try? await ChromaticGlitchShaderMath.shader(amount: 1, time: 0, bounds: .zero).compile(as: .layerEffect)
}
}
}
// MARK: - Demo
struct ChromaticGlitchShaderDemo: View {
@State private var amount = 0.0
/// Only for this demo: it plays by itself until someone touches it.
@State private var isAutoPlaying = true
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
VStack(spacing: 36) {
VStack(spacing: 14) {
Image(systemName: "antenna.radiowaves.left.and.right")
.font(.system(size: 64, weight: .semibold))
.accessibilityHidden(true)
Text("SIGNAL LOST")
.font(.system(.largeTitle, design: .monospaced, weight: .heavy))
Text("Reconnecting to channel 7")
.font(.system(.subheadline, design: .monospaced))
.opacity(0.7)
}
.foregroundStyle(.white)
.padding(.vertical, 44)
.frame(maxWidth: .infinity)
.background(LinearGradient(colors: [.cyan, .blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing), in: .rect(cornerRadius: 28))
.chromaticGlitch(amount)
VStack(alignment: .leading, spacing: 6) {
Text("Glitch")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.white.opacity(0.7))
Slider(value: $amount, in: 0...1)
.tint(.cyan)
.accessibilityLabel("Glitch")
}
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(red: 0.04, green: 0.04, blue: 0.09).ignoresSafeArea())
.simultaneousGesture(DragGesture(minimumDistance: 0).onChanged { _ in isAutoPlaying = false })
.task {
while isAutoPlaying {
try? await Task.sleep(for: .seconds(0.9))
guard !Task.isCancelled, isAutoPlaying else { return }
withAnimation(reduceMotion ? nil : .easeIn(duration: 0.25)) { amount = 1 }
try? await Task.sleep(for: .seconds(1.1))
guard !Task.isCancelled, isAutoPlaying else { return }
withAnimation(reduceMotion ? nil : .easeOut(duration: 0.6)) { amount = 0 }
}
}
}
}
#Preview {
ChromaticGlitchShaderDemo()
}
ChromaticGlitchShader.metal
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
#include <metal_stdlib>
#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;
// The color of the view at a point, or nothing outside it. Without this a band
// that moves past an edge would drag the last color of the view along with it.
static half4 ChromaticGlitchShaderSample(SwiftUI::Layer layer, float2 point, float2 bounds) {
if (point.x < 0.0 || point.y < 0.0 || point.x > bounds.x || point.y > bounds.y) {
return half4(0.0);
}
return layer.sample(point);
}
// For SwiftUI's `layerEffect`. Splits the view into its red, green and blue,
// each a little to one side, shifts some horizontal bands sideways and draws
// faint scan lines over it. `amount` goes from 0, which leaves the view as it
// is, to 1. `time` picks which bands are shifted, and they change `rate` times
// a second. `split` and `shift` are the farthest a channel and a band move, in
// points, and `bounds` is the size of the view.
[[ stitchable ]] half4 ChromaticGlitchShaderSplit(
float2 position,
SwiftUI::Layer layer,
float amount,
float time,
float split,
float shift,
float2 bounds
) {
if (amount <= 0.0) {
return layer.sample(position);
}
float rate = 10.0;
float band = floor(position.y / 22.0);
float noise = fract(sin(band * 91.7 + floor(time * rate) * 37.3) * 43758.5453);
float moved = noise > 0.7 ? (noise - 0.7) / 0.3 : 0.0;
float direction = noise > 0.85 ? -1.0 : 1.0;
float2 point = float2(position.x + moved * direction * shift * amount, position.y);
float2 offset = float2(split * amount, 0.0);
half4 red = ChromaticGlitchShaderSample(layer, point + offset, bounds);
half4 green = ChromaticGlitchShaderSample(layer, point, bounds);
half4 blue = ChromaticGlitchShaderSample(layer, point - offset, bounds);
half4 color = half4(red.r, green.g, blue.b, max(max(red.a, green.a), blue.a));
float lines = 1.0 - 0.14 * amount * step(0.5, fract(position.y / 3.0));
color.rgb *= half(lines);
return color;
}