Table of Contents
- The problem
- Design decisions and alternatives
- What went wrong: green checks that tested nothing
- Usage
- Limits
The problem
SwiftUI's Liquid Glass API, glassEffect, exists only on iOS, macOS, tvOS and watchOS 26. An app
whose users are still partly on iOS 17 or 18 has two options: ship the glass look only to OS 26
users, or wrap every glass view in an if #available(iOS 26, *) branch with a hand-made
alternative in the else. The second option spreads the same decision over every screen, and
each copy tends to drift: one card forgets the theme, another ignores Reduce Transparency.
LiquidGlassKit moves that decision into one place. On OS 26 it draws with SwiftUI's native
glassEffect; on earlier versions (back to iOS 17, macOS 14, tvOS 17 and watchOS 10) and on
visionOS it draws a Material blur with a tint, an edge highlight and a shadow. You write the same
code for every OS. The package has no third-party dependencies and is distributed through Swift
Package Manager only.
Design decisions and alternatives
One entry point. Only the Surface folder calls SwiftUI's glass API: the
glassSurface(_:in:interactive:) modifier and GlassGroup, which wraps GlassEffectContainer on
the native path. Every component (buttons, cards, text fields, the search bar, the tab bar, the
navigation bar, the sheet, and since 2.1.0 the switch and the slider) is drawn through
glassSurface. The alternative was a separate #available branch per component. The February
2026 code had no shared path: each component filled its own Material
(.fill(material.swiftUIMaterial)), and the 2.0.0 changelog records the result: components that
ignored the theme. The cost of the single entry point is that a component-specific look, such as a different
shadow for buttons, needs a new parameter on the shared modifier.
Staying out of SwiftUI's namespace. The February code defined its own glassEffect(_:) and
GlassEffectContainer. Against the OS 26 SDK those names collide with SwiftUI's, and a file that
imports both modules gets ambiguous calls. The rebuilt API uses glassSurface, GlassGroup and
LiquidGlassButtonStyle (SwiftUI already has a GlassButtonStyle). Users learn the package's names
instead of Apple's; the README and the DocC catalog name the SwiftUI API each one wraps.
Choosing the rendering path in three layers. #if os(visionOS) removes the native call at
compile time, because SwiftUI marks glassEffect unavailable there. A runtime
#available(iOS 26, macOS 26, tvOS 26, watchOS 26, *) check lets one binary run on iOS 17 and
iOS 26. On top of that, GlassRenderingMode (automatic or fallback) comes from the
environment, so an app can force the fallback for a consistent look. The decision itself is a pure
function, resolved(nativeAvailable:), and all four combinations are tested. An early design also
had a .native case; it was removed before 2.0.0 (6293c16) because it always behaved like
.automatic: native glass cannot be forced where it does not exist, and where it exists
.automatic already uses it.
An enum for shapes. GlassShape is an enum (rounded rectangle, capsule, circle, per-corner
radii) rather than a generic Shape, so it can live inside GlassTheme, be Hashable in tests
and be Sendable under Swift 6. Drawing goes through AnyShape. The trade-off is real: you cannot
pass your own shape.
Styles instead of custom views. Buttons are a ButtonStyle (.buttonStyle(.liquidGlass)) and
the switch is a ToggleStyle (.toggleStyle(.liquidGlass)). Pressed state comes from
configuration.isPressed, and keyboard and VoiceOver behavior stays that of a normal SwiftUI
Button or Toggle. GlassButton remains as a convenience view that uses the same style.
Merging by rewriting. Version 2.1.0 takes ideas from two earlier repositories by the same
author, LiquidGlass-Playground and GlassmorphismUI, both MIT licensed. Their code drew its own
Material layers and depended on UIKit for haptics and accessibility checks. Instead of copying
files, each feature (switch, slider, gradient glass, highlights, presets, the code exporter, tab
badges) was rewritten on top of glassSurface, so it picks up the theme, the rendering mode and
Reduce Transparency. Some features were left out on purpose: blur radius, refraction and
saturation values have no counterpart in native glass. `swift package
diagnose-api-breaking-changes v2.0.0` reports no breaking changes, so the release is a minor
version. Both source repositories are now archived and point here.
What went wrong: green checks that tested nothing
The repository's history is not a straight line, and the changelog says so. The v1.0.0 tag from
February 2026 points at the first commit, which does not build. In June 2026 most of the source was
replaced by placeholders, the test target referenced types that did not exist, and CI still showed
green because the test and lint steps had continue-on-error. Version 2.0.0 rebuilt the package
and removed that flag; no workflow in the repository uses it today.
The more useful lesson came after the rebuild, when the new tests were already green. Removing
features one at a time from the source showed that the suite did not notice. Commit 36ef829
describes it: the sheet test only compared the image width with the frame width, which the frame
sets anyway, and removing the glass from GlassCard or GlassTabBar, ignoring a card's material,
or drawing dark button labels in black again all left the suite green. The tests were rewritten to
count pixels. Views are drawn on a white canvas with a green theme, and every component must draw
at least 250 green pixels on the fallback path; an explicit blue material must win over the green
theme. Two ImageRenderer quirks shaped the design: on macOS 27 a view that draws nothing can
return a buffer holding an earlier render, so transparency alone is not a signal; and text fields
render as a red and yellow placeholder, so red was not usable as a test tint. After the change,
each of 11 source mutations listed in the commit fails at least one test.
The native path had the same blind spot. On OS 26, the render test only required an image. With
the native view changed to always draw its fallback, all 39 tests still passed. Since
ImageRenderer does not capture the blur and draws native glass differently per OS, a tint check
could not work everywhere. Commit 29ce746 renders each component on both paths instead: two
fallback renders of the same view are pixel-identical, and a native render must differ from the
fallback in more than 250 pixels. The commit records at least 3,857 differing pixels per component
on macOS 27 and 5,005 on the iOS 26.5 and 27 simulators.
One more problem appeared during 2.1.0. Pixel-equality tests failed in about one full run in four,
and one failed every time with swift test --no-parallel. Commit 68f94e8 found two causes:
ImageRenderer caches its image, and the first renders of a view differ from later ones while
Material edges and glyphs settle. The test helper now uses a new renderer per pass, warms up for
eight passes and stops when two consecutive passes match, with a limit of 16.
Usage
Add the package in Xcode with File › Add Package Dependencies… and the URL
https://github.com/muhittincamdali/LiquidGlassKit.git, or in Package.swift. The README's
quick start, which the test target compiles verbatim:
1import LiquidGlassKit2import SwiftUI3 4struct ContentView: View {5 var body: some View {6 ZStack {7 LinearGradient(colors: [.indigo, .pink], startPoint: .top, endPoint: .bottom)8 .ignoresSafeArea()9 VStack(spacing: 16) {10 GlassCard {11 Label("Hello, glass", systemImage: "sparkles")12 }13 GlassButton("Tap me", systemImage: "hand.tap") {}14 }15 }16 }17}Any view can sit on glass with one modifier:
1import LiquidGlassKit2import SwiftUI3 4struct NowPlayingBadge: View {5 var body: some View {6 Label("Now Playing", systemImage: "music.note")7 .padding()8 .glassSurface(.tinted(.purple), in: .capsule)9 }10}A theme and a rendering mode apply to a whole hierarchy:
1struct ThemedApp: View {2 var body: some View {3 RootView()4 .glassTheme(GlassTheme(material: .tinted(.indigo), shape: .roundedRectangle(cornerRadius: 12)))5 .glassRenderingMode(.automatic) // or .fallback to use the Material look everywhere6 }7}A preset can be adjusted and exported as Swift source that uses the package:
1var theme = GlassPreset.named("Ocean")?.theme ?? .standard2theme.shadowRadius = 43let source = GlassCodeExporter().viewCode(named: "PromoCard", theme: theme)The demo app in Examples/LiquidGlassDemo shows a gallery, tab bars, the sheet, a native versus
fallback comparison and a playground that tunes a preset and shows its exported code. The DocC
articles are published on GitHub Pages.
Limits
- The final glass look is not tested.
ImageRendererdoes not capture the blur and captures
native glass only in part. The tests check the fallback's tint and coverage, and that the native
path renders differently from the fallback; the README states this.
- Test coverage of devices. CI runs the suite on macOS, an iOS 26 simulator and an iOS 18.6
simulator, where only the fallback exists. There is no iOS 17 job and no run on a physical
device.
- No performance numbers. The repository has no benchmark or profiling results for the
fallback blur and shadows.
- visionOS uses the fallback only, because SwiftUI does not offer
glassEffectthere. GlassSheetis an overlay, not a system sheet: no detents, no separate presentation context,
and it is not available on tvOS. GlassSlider is not available on tvOS either.
- Fixed shapes.
GlassShapehas four cases; custom shapes are not supported. - Localization. Titles of
GlassButton,GlassTabandGlassTextFieldareStringvalues
shown verbatim, so no LocalizedStringKey lookup happens, and the package ships no localized
strings of its own.
- Toolchain. Xcode 26 or later is required even for older deployment targets, because the
native path is compiled against the OS 26 SDK.
The current release is 2.1.0, and the README says no later release is scheduled yet.
Source code: github.com/muhittincamdali/LiquidGlassKit
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
Your subscription starts when you open the link in the confirmation email and press “Confirm my subscription”. The newsletter keeps open/click statistics; you can unsubscribe anytime with one click. Privacy

