A SwiftUI library that uses Metal to display efficient progressive blurs, just like the ones used by Apple. No CIFilter, and no private APIs used.
This repository is a Swift package, so just include it in your Xcode project and target under File > Add package dependencies. Then, import Glur to the Swift files where you'll be using it.
The package vends two products, and you choose which ones your target links against:
| Product | What you get | Blurs | Private API |
|---|---|---|---|
Glur |
The .glur() modifier and GlurMask |
The view it's applied to | None |
GlurBackdrop |
GlurView |
The content behind it | Yes — see Blurring the backdrop |
GlurBackdrop depends on Glur, so masks are shared between them. Nothing in GlurBackdrop reaches your binary unless you add that product explicitly.
Note
While Glur is supported on older platforms, it will only utilize the Metal implementation of the blur effect on iOS 17.0 and later, macOS 14.0 and later, and tvOS 17.0 and later. Otherwise, it will present a worse, compatibility effect that should be tested by the developer before being used in production.
The Metal implementation is not available on watchOS, and therefore the compatibility effect will be presented on this platform by default.
You can add a glur effect with the following modifier:
.glur()Here are all optional parameters:
.glur(radius: 8.0, // The total radius of the blur effect when fully applied.
offset: 0.3, // The distance from the view's edge to where the effect begins, relative to the view's size.
interpolation: 0.4, // The distance from the offset to where the effect is fully applied, relative to the view's size.
direction: .down, // The direction in which the effect is applied.
noise: 0.1, // The amount of noise that should be applied to the view.
drawingGroup: true // Whether or not to pre-render the modified view with `drawingGroup()`.
)Where the effect lands is described by a GlurMask, a grayscale ramp in which an intensity of 0 leaves the view untouched and an intensity of 1 applies the full radius. The parameters above are shorthand for the default one, so these two are the same effect:
.glur(radius: 8.0, offset: 0.3, interpolation: 0.4, direction: .down)
.glur(radius: 8.0, mask: .linear(direction: .down, offset: 0.3, interpolation: 0.4))Passing a mask directly opens up the shapes the shorthand can't spell:
.glur(radius: 8.0, mask: .radial(offset: 0.2, interpolation: 0.5)) // Sharp in the middle, blurred towards the corners
.glur(radius: 8.0, mask: .radial(offset: 0.2, interpolation: 0.5, spread: 2.5)) // The same, reaching much further out
.glur(radius: 8.0, mask: .linear(stops: [.init(intensity: 1.0, location: 0.0), // Blurred at both ends, sharp in the middle
.init(intensity: 0.0, location: 0.5),
.init(intensity: 1.0, location: 1.0)]))spread is how far a radial mask reaches, as a multiple of the view's longest side. At 1.0 the ramp finishes at the far edge; larger values push it outwards, so the falloff is wider and gentler and the corners never reach the full radius.
Every mask is a SwiftUI gradient underneath — .linear is a LinearGradient, .radial a RadialGradient — drawn into a square with ImageRenderer and stretched to fit. .view() hands that same pipeline something of your own, which covers everything the built-in ramps can't spell: angular, elliptical and mesh gradients, shapes, text, images.
.glur(radius: 8.0, mask: .view(AngularGradient(colors: [.black, .clear], center: .center)))
.glur(radius: 8.0, mask: .view(Text("blur").font(.system(size: 96, weight: .black))))
.glur(radius: 8.0, mask: .image(myCGImage)) // Or a bitmap you already haveOnly opacity matters, whichever mask you use. It's flattened to white carrying that alpha, so colors are ignored and .black to .clear reads the same as .white to .clear.
Note
The built-in gradients depend only on their own parameters, never on the size of the view, so they're drawn once and cached. A .view() mask can't be cached that way and is redrawn whenever the effect updates — prefer a built-in mask where one will do.
Warning
When being used in the iOS simulator, SwiftUI shader effects may not be displayed if the view exceeds 545 points in either dimension. Please note that, on a physical device, the effect should work as intented.
The modifier blurs the view it's applied to, which means it can only reach views SwiftUI draws itself. GlurView is the other half: a transparent overlay that blurs whatever is rendered behind it, including ScrollView and other platform-backed content.
It ships as a separate module, so nothing below arrives in your binary unless you ask for it:
import GlurBackdrop
content
.overlay(alignment: .top) {
GlurView(radius: 12.0, offset: 0.0, interpolation: 1.0, direction: .up)
.frame(height: 120)
}It takes the same masks as the modifier, so GlurView(radius: 12.0, mask: .radial()) works too.
Note
GlurView requires iOS 16.0, macOS 13.0 or tvOS 16.0. Masks are rasterized with ImageRenderer, which starts there. The .glur() modifier is unaffected, since its Metal path starts later still and its compatibility effect never rasterizes.
Warning
This reaches a private API on every platform it supports — nothing public applies a varying blur to a backdrop. Classes are looked up by name at runtime and the names are held as code units, so no readable literal ends up in the binary, but that is obfuscation rather than a guarantee. This is why it lives in its own module. Weigh the App Store risk yourself before shipping it.
On watchOS the view renders as empty space.
macOS looks like it should avoid this, and doesn't. CALayer.backgroundFilters is genuinely supported there, and Core Image ships CIMaskedVariableBlur — but that pair only works for an AppKit layer hierarchy. SwiftUI draws its content into its own hosting layer rather than into sibling layers beneath the view, so nothing is composited behind it and the filter runs against nothing. NSVisualEffectView is no help either: it has no in-process backdrop layer to borrow, because its blur happens out of process.
This project builds on a proof of concept developed in June of 2023, right after WWDC.
It makes use of Apple's new simplified Shader API for SwiftUI. First, I coded a Metal shader that produced a gaussian blur for the modified view with the correct gaussian weights distribution, efficiently. Then, I modified it slightly to vary the blur radius over the vertical or horizontal axis given the offset, interpolation and direction values.
Note
The shader runs through Apple's Shader API, which only reaches views SwiftUI draws itself. The .glur() modifier therefore can't be applied to platform-backed views such as ScrollView, TextEditor or Map — on those it silently does nothing.
That's what GlurView is for: rather than blurring the view it's applied to, it blurs whatever is behind it, so you can lay one over a ScrollView instead of trying to apply the effect to it.
Tip
If you want to learn how to write your first Metal shader with SwiftUI, check out this tutorial that I wrote for the Cindori blog.
You can run a demo of Glur in your device or simulator through the GlurDemo project in this repository.