Apple's Liquid Glass design system for iOS 26+ with backward-compatible fallbacks for iOS 17-25...
Implement Apple's Liquid Glass design system introduced in iOS 26 (WWDC25) with graceful fallbacks for iOS 17-25. This skill covers adaptive glass styling, button styles, container effects, and the architectural patterns for building version-aware SwiftUI components.
.glassProminent with tinting.glass without tinting| Element Type | iOS 26 Style | Fallback Style |
|---|---|---|
| Primary action | .buttonStyle(.glassProminent) |
Filled capsule with color |
| Secondary action | .buttonStyle(.glass) |
Semi-transparent capsule |
| Status badge | .glassEffect() |
Color.black.opacity(0.6) capsule |
| Card container | .glassEffect() |
Color.black.opacity(0.7) rounded rect |
Direct if #available checks in SwiftUI body cause performance issues due to view diffing. Instead, use wrapper components with separate iOS 26+ and fallback implementations.
// ❌ AVOID: if #available in body causes performance issues
struct BadButton: View {
var body: some View {
if #available(iOS 26.0, *) {
Button("Tap") { }
.buttonStyle(.glass) // Recomputed every render
} else {
Button("Tap") { }
.background(Capsule().fill(.black.opacity(0.6)))
}
}
}
// ✅ CORRECT: Wrapper component with separate implementations
struct GlassButton: View {
let title: String
let action: () -> Void
var body: some View {
if #available(iOS 26.0, *) {
GlassButtoniOS26(title: title, action: action)
} else {
GlassButtonFallback(title: title, action: action)
}
}
}
extension View {
/// Applies glass effect on iOS 26+, or semi-transparent capsule on older versions
/// Use for badges, status indicators, and small UI elements
@ViewBuilder
func adaptiveGlass() -> some View {
if #available(iOS 26.0, *) {
self.glassEffect()
} else {
self.background(Capsule().fill(Color.black.opacity(0.6)))
}
}
/// Applies glass effect with rounded rectangle on iOS 26+, or fallback styling
/// Use for cards and larger container elements
@ViewBuilder
func adaptiveGlassCard() -> some View {
if #available(iOS 26.0, *) {
self.glassEffect()
} else {
self.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.black.opacity(0.7))
)
}
}
}
// Status badge with glass effect
Text("LIVE")
.font(.caption.bold())
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.adaptiveGlass()
// Card with glass effect
VStack {
Text("Stream Info")
Text("1920x1080 @ 30fps")
}
.padding()
.adaptiveGlassCard()
/// Primary action button with glass prominent style (iOS 26+)
@available(iOS 26.0, *)
struct GlassProminentButtoniOS26: View {
let title: String
let icon: String
var tintColor: Color = .blue
let action: () -> Void
var body: some View {
Button(action: action) {
Label(title, systemImage: icon)
.font(.title2.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
}
.buttonStyle(.glassProminent)
.tint(tintColor)
}
}
/// Primary action button fallback (iOS 17-25)
struct GlassProminentButtonFallback: View {
let title: String
let icon: String
var tintColor: Color = .blue
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.title2)
Text(title)
.font(.title3.bold())
}
.foregroundColor(.white)
.padding(.horizontal, 28)
.padding(.vertical, 18)
.background(
Capsule()
.fill(tintColor.opacity(0.85))
)
}
}
}
/// Wrapper that chooses the right implementation
struct GlassProminentButton: View {
let title: String
let icon: String
var tintColor: Color = .blue
let action: () -> Void
var body: some View {
if #available(iOS 26.0, *) {
GlassProminentButtoniOS26(title: title, icon: icon, tintColor: tintColor, action: action)
} else {
GlassProminentButtonFallback(title: title, icon: icon, tintColor: tintColor, action: action)
}
}
}
/// Secondary action button with glass style (iOS 26+)
@available(iOS 26.0, *)
struct GlassButtoniOS26: View {
let title: String
let icon: String
let action: () -> Void
var body: some View {
Button(action: action) {
Label(title, systemImage: icon)
.font(.title3.weight(.semibold))
.padding(.horizontal, 6)
.padding(.vertical, 2)
}
.buttonStyle(.glass)
}
}
/// Secondary action button fallback (iOS 17-25)
struct GlassButtonFallback: View {
let title: String
let icon: String
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.title3)
Text(title)
.font(.title3.weight(.semibold))
}
.foregroundColor(.white)
.padding(.horizontal, 24)
.padding(.vertical, 16)
.background(
Capsule()
.fill(Color.black.opacity(0.6))
)
}
}
}
/// Wrapper that chooses the right implementation
struct GlassButton: View {
let title: String
let icon: String
let action: () -> Void
var body: some View {
if #available(iOS 26.0, *) {
GlassButtoniOS26(title: title, icon: icon, action: action)
} else {
GlassButtonFallback(title: title, icon: icon, action: action)
}
}
}
For full-width primary actions (like Start/Stop streaming), use .ultraThinMaterial with a color overlay instead of pure glass:
struct FullWidthBottomButton: View {
let title: String
let icon: String
var tintColor: Color = .blue
var isLoading: Bool = false
let action: () -> Void
private var isLargeScreen: Bool {
UIDevice.current.userInterfaceIdiom == .pad
}
var body: some View {
Button(action: action) {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.title2)
Text(title)
.font(.title3.bold())
}
.foregroundColor(.white)
.frame(maxWidth: isLargeScreen ? 300 : .infinity)
.padding(.vertical, 18)
.padding(.horizontal, 24)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 16)
.fill(tintColor.opacity(0.3))
)
)
}
.disabled(isLoading)
.padding(.horizontal, isLargeScreen ? 0 : 16)
.padding(.bottom, isLargeScreen ? 40 : 24)
}
}
GitHub Actions and most CI services use stable Xcode versions. iOS 26 SDK requires Xcode 26, which may not be available in CI when iOS 26 is first released.
// Option 1: Runtime availability check (always compiles)
if #available(iOS 26.0, *) {
self.glassEffect()
}
// Option 2: Compiler version check (for APIs that don't exist in older SDKs)
#if compiler(>=6.2) // Swift 6.2 = Xcode 26
.buttonStyle(.glassProminent)
#else
.buttonStyle(.borderedProminent)
#endif
Use runtime #available checks that compile on any Xcode version, with fallback implementations that look good on iOS 17-25:
// This compiles on Xcode 15/16 AND Xcode 26
// On iOS 26+: Uses native glass effect
// On iOS 17-25: Uses semi-transparent capsule
@ViewBuilder
func adaptiveGlass() -> some View {
if #available(iOS 26.0, *) {
self.glassEffect()
} else {
self.background(Capsule().fill(Color.black.opacity(0.6)))
}
}
struct StatusBadge: View {
let isLive: Bool
let text: String
var body: some View {
HStack(spacing: 6) {
Circle()
.fill(isLive ? Color.red : Color.gray)
.frame(width: 8, height: 8)
Text(text)
.font(.caption.bold())
.foregroundColor(.white)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.adaptiveGlass()
}
}
// Usage
StatusBadge(isLive: true, text: "LIVE")
StatusBadge(isLive: false, text: "OFFLINE")
struct InfoCard: View {
let title: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.caption)
.foregroundColor(.secondary)
Text(value)
.font(.headline)
.foregroundColor(.primary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.adaptiveGlassCard()
}
}
struct StreamingControlView: View {
@State private var isStreaming = false
var body: some View {
ZStack {
// Camera preview background
CameraPreviewView()
VStack {
// Top status bar with glass badges
HStack {
StatusBadge(isLive: isStreaming, text: isStreaming ? "LIVE" : "READY")
Spacer()
// Settings button with glass style
GlassButton(title: "Settings", icon: "gear") {
// Open settings
}
}
.padding()
Spacer()
// Bottom action button
if isStreaming {
GlassProminentButton(
title: "Stop Streaming",
icon: "stop.fill",
tintColor: .red
) {
isStreaming = false
}
} else {
GlassProminentButton(
title: "Start Streaming",
icon: "play.fill",
tintColor: .green
) {
isStreaming = true
}
}
}
}
}
}
| Use Case | Fallback Background |
|---|---|
| Dark overlay (camera UI) | Color.black.opacity(0.6) |
| Card background | Color.black.opacity(0.7) |
| Light mode badge | Color.white.opacity(0.8) |
| Tinted primary button | tintColor.opacity(0.85) |
// ❌ DON'T: Multiple glass layers
VStack {
Text("Hello")
.adaptiveGlass()
}
.adaptiveGlassCard()
// ✅ DO: Single glass layer
VStack {
Text("Hello")
}
.adaptiveGlassCard()
// ❌ DON'T: Glass buttons shouldn't be full-width
Button("Start") { }
.frame(maxWidth: .infinity)
.buttonStyle(.glass)
// ✅ DO: Content-hugging capsule shape
Button("Start") { }
.buttonStyle(.glass) // Capsule is default
// ❌ DON'T: Will crash on iOS 17-25
Button("Tap") { }
.buttonStyle(.glass)
// ✅ DO: Always check availability
if #available(iOS 26.0, *) {
Button("Tap") { }
.buttonStyle(.glass)
} else {
Button("Tap") { }
.background(Capsule().fill(.black.opacity(0.6)))
}
See CarSeet/CarSeet/ContentView.swift for the complete production implementation including:
adaptiveGlass() and adaptiveGlassCard() view extensionsGlassProminentButton and GlassButton wrapper componentsFullWidthBottomButton for streaming controls