280 lines
8.8 KiB
Markdown
280 lines
8.8 KiB
Markdown
|
|
---
|
||
|
|
name: liquid-glass-design
|
||
|
|
description: iOS 26 Liquid Glass design system — dynamic glass material with blur, reflection, and interactive morphing for SwiftUI, UIKit, and WidgetKit.
|
||
|
|
---
|
||
|
|
|
||
|
|
# Liquid Glass Design System (iOS 26)
|
||
|
|
|
||
|
|
Patterns for implementing Apple's Liquid Glass — a dynamic material that blurs content behind it, reflects color and light from surrounding content, and reacts to touch and pointer interactions. Covers SwiftUI, UIKit, and WidgetKit integration.
|
||
|
|
|
||
|
|
## When to Activate
|
||
|
|
|
||
|
|
- Building or updating apps for iOS 26+ with the new design language
|
||
|
|
- Implementing glass-style buttons, cards, toolbars, or containers
|
||
|
|
- Creating morphing transitions between glass elements
|
||
|
|
- Applying Liquid Glass effects to widgets
|
||
|
|
- Migrating existing blur/material effects to the new Liquid Glass API
|
||
|
|
|
||
|
|
## Core Pattern — SwiftUI
|
||
|
|
|
||
|
|
### Basic Glass Effect
|
||
|
|
|
||
|
|
The simplest way to add Liquid Glass to any view:
|
||
|
|
|
||
|
|
```swift
|
||
|
|
Text("Hello, World!")
|
||
|
|
.font(.title)
|
||
|
|
.padding()
|
||
|
|
.glassEffect() // Default: regular variant, capsule shape
|
||
|
|
```
|
||
|
|
|
||
|
|
### Customizing Shape and Tint
|
||
|
|
|
||
|
|
```swift
|
||
|
|
Text("Hello, World!")
|
||
|
|
.font(.title)
|
||
|
|
.padding()
|
||
|
|
.glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 16.0))
|
||
|
|
```
|
||
|
|
|
||
|
|
Key customization options:
|
||
|
|
- `.regular` — standard glass effect
|
||
|
|
- `.tint(Color)` — add color tint for prominence
|
||
|
|
- `.interactive()` — react to touch and pointer interactions
|
||
|
|
- Shape: `.capsule` (default), `.rect(cornerRadius:)`, `.circle`
|
||
|
|
|
||
|
|
### Glass Button Styles
|
||
|
|
|
||
|
|
```swift
|
||
|
|
Button("Click Me") { /* action */ }
|
||
|
|
.buttonStyle(.glass)
|
||
|
|
|
||
|
|
Button("Important") { /* action */ }
|
||
|
|
.buttonStyle(.glassProminent)
|
||
|
|
```
|
||
|
|
|
||
|
|
### GlassEffectContainer for Multiple Elements
|
||
|
|
|
||
|
|
Always wrap multiple glass views in a container for performance and morphing:
|
||
|
|
|
||
|
|
```swift
|
||
|
|
GlassEffectContainer(spacing: 40.0) {
|
||
|
|
HStack(spacing: 40.0) {
|
||
|
|
Image(systemName: "scribble.variable")
|
||
|
|
.frame(width: 80.0, height: 80.0)
|
||
|
|
.font(.system(size: 36))
|
||
|
|
.glassEffect()
|
||
|
|
|
||
|
|
Image(systemName: "eraser.fill")
|
||
|
|
.frame(width: 80.0, height: 80.0)
|
||
|
|
.font(.system(size: 36))
|
||
|
|
.glassEffect()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The `spacing` parameter controls merge distance — closer elements blend their glass shapes together.
|
||
|
|
|
||
|
|
### Uniting Glass Effects
|
||
|
|
|
||
|
|
Combine multiple views into a single glass shape with `glassEffectUnion`:
|
||
|
|
|
||
|
|
```swift
|
||
|
|
@Namespace private var namespace
|
||
|
|
|
||
|
|
GlassEffectContainer(spacing: 20.0) {
|
||
|
|
HStack(spacing: 20.0) {
|
||
|
|
ForEach(symbolSet.indices, id: \.self) { item in
|
||
|
|
Image(systemName: symbolSet[item])
|
||
|
|
.frame(width: 80.0, height: 80.0)
|
||
|
|
.glassEffect()
|
||
|
|
.glassEffectUnion(id: item < 2 ? "group1" : "group2", namespace: namespace)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Morphing Transitions
|
||
|
|
|
||
|
|
Create smooth morphing when glass elements appear/disappear:
|
||
|
|
|
||
|
|
```swift
|
||
|
|
@State private var isExpanded = false
|
||
|
|
@Namespace private var namespace
|
||
|
|
|
||
|
|
GlassEffectContainer(spacing: 40.0) {
|
||
|
|
HStack(spacing: 40.0) {
|
||
|
|
Image(systemName: "scribble.variable")
|
||
|
|
.frame(width: 80.0, height: 80.0)
|
||
|
|
.glassEffect()
|
||
|
|
.glassEffectID("pencil", in: namespace)
|
||
|
|
|
||
|
|
if isExpanded {
|
||
|
|
Image(systemName: "eraser.fill")
|
||
|
|
.frame(width: 80.0, height: 80.0)
|
||
|
|
.glassEffect()
|
||
|
|
.glassEffectID("eraser", in: namespace)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Button("Toggle") {
|
||
|
|
withAnimation { isExpanded.toggle() }
|
||
|
|
}
|
||
|
|
.buttonStyle(.glass)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Extending Horizontal Scrolling Under Sidebar
|
||
|
|
|
||
|
|
To allow horizontal scroll content to extend under a sidebar or inspector, ensure the `ScrollView` content reaches the leading/trailing edges of the container. The system automatically handles the under-sidebar scrolling behavior when the layout extends to the edges — no additional modifier is needed.
|
||
|
|
|
||
|
|
## Core Pattern — UIKit
|
||
|
|
|
||
|
|
### Basic UIGlassEffect
|
||
|
|
|
||
|
|
```swift
|
||
|
|
let glassEffect = UIGlassEffect()
|
||
|
|
glassEffect.tintColor = UIColor.systemBlue.withAlphaComponent(0.3)
|
||
|
|
glassEffect.isInteractive = true
|
||
|
|
|
||
|
|
let visualEffectView = UIVisualEffectView(effect: glassEffect)
|
||
|
|
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||
|
|
visualEffectView.layer.cornerRadius = 20
|
||
|
|
visualEffectView.clipsToBounds = true
|
||
|
|
|
||
|
|
view.addSubview(visualEffectView)
|
||
|
|
NSLayoutConstraint.activate([
|
||
|
|
visualEffectView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||
|
|
visualEffectView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||
|
|
visualEffectView.widthAnchor.constraint(equalToConstant: 200),
|
||
|
|
visualEffectView.heightAnchor.constraint(equalToConstant: 120)
|
||
|
|
])
|
||
|
|
|
||
|
|
// Add content to contentView
|
||
|
|
let label = UILabel()
|
||
|
|
label.text = "Liquid Glass"
|
||
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||
|
|
visualEffectView.contentView.addSubview(label)
|
||
|
|
NSLayoutConstraint.activate([
|
||
|
|
label.centerXAnchor.constraint(equalTo: visualEffectView.contentView.centerXAnchor),
|
||
|
|
label.centerYAnchor.constraint(equalTo: visualEffectView.contentView.centerYAnchor)
|
||
|
|
])
|
||
|
|
```
|
||
|
|
|
||
|
|
### UIGlassContainerEffect for Multiple Elements
|
||
|
|
|
||
|
|
```swift
|
||
|
|
let containerEffect = UIGlassContainerEffect()
|
||
|
|
containerEffect.spacing = 40.0
|
||
|
|
|
||
|
|
let containerView = UIVisualEffectView(effect: containerEffect)
|
||
|
|
|
||
|
|
let firstGlass = UIVisualEffectView(effect: UIGlassEffect())
|
||
|
|
let secondGlass = UIVisualEffectView(effect: UIGlassEffect())
|
||
|
|
|
||
|
|
containerView.contentView.addSubview(firstGlass)
|
||
|
|
containerView.contentView.addSubview(secondGlass)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Scroll Edge Effects
|
||
|
|
|
||
|
|
```swift
|
||
|
|
scrollView.topEdgeEffect.style = .automatic
|
||
|
|
scrollView.bottomEdgeEffect.style = .hard
|
||
|
|
scrollView.leftEdgeEffect.isHidden = true
|
||
|
|
```
|
||
|
|
|
||
|
|
### Toolbar Glass Integration
|
||
|
|
|
||
|
|
```swift
|
||
|
|
let favoriteButton = UIBarButtonItem(image: UIImage(systemName: "heart"), style: .plain, target: self, action: #selector(favoriteAction))
|
||
|
|
favoriteButton.hidesSharedBackground = true // Opt out of shared glass background
|
||
|
|
```
|
||
|
|
|
||
|
|
## Core Pattern — WidgetKit
|
||
|
|
|
||
|
|
### Rendering Mode Detection
|
||
|
|
|
||
|
|
```swift
|
||
|
|
struct MyWidgetView: View {
|
||
|
|
@Environment(\.widgetRenderingMode) var renderingMode
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
if renderingMode == .accented {
|
||
|
|
// Tinted mode: white-tinted, themed glass background
|
||
|
|
} else {
|
||
|
|
// Full color mode: standard appearance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Accent Groups for Visual Hierarchy
|
||
|
|
|
||
|
|
```swift
|
||
|
|
HStack {
|
||
|
|
VStack(alignment: .leading) {
|
||
|
|
Text("Title")
|
||
|
|
.widgetAccentable() // Accent group
|
||
|
|
Text("Subtitle")
|
||
|
|
// Primary group (default)
|
||
|
|
}
|
||
|
|
Image(systemName: "star.fill")
|
||
|
|
.widgetAccentable() // Accent group
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Image Rendering in Accented Mode
|
||
|
|
|
||
|
|
```swift
|
||
|
|
Image("myImage")
|
||
|
|
.widgetAccentedRenderingMode(.monochrome)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Container Background
|
||
|
|
|
||
|
|
```swift
|
||
|
|
VStack { /* content */ }
|
||
|
|
.containerBackground(for: .widget) {
|
||
|
|
Color.blue.opacity(0.2)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Key Design Decisions
|
||
|
|
|
||
|
|
| Decision | Rationale |
|
||
|
|
|----------|-----------|
|
||
|
|
| GlassEffectContainer wrapping | Performance optimization, enables morphing between glass elements |
|
||
|
|
| `spacing` parameter | Controls merge distance — fine-tune how close elements must be to blend |
|
||
|
|
| `@Namespace` + `glassEffectID` | Enables smooth morphing transitions on view hierarchy changes |
|
||
|
|
| `interactive()` modifier | Explicit opt-in for touch/pointer reactions — not all glass should respond |
|
||
|
|
| UIGlassContainerEffect in UIKit | Same container pattern as SwiftUI for consistency |
|
||
|
|
| Accented rendering mode in widgets | System applies tinted glass when user selects tinted Home Screen |
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
- **Always use GlassEffectContainer** when applying glass to multiple sibling views — it enables morphing and improves rendering performance
|
||
|
|
- **Apply `.glassEffect()` after** other appearance modifiers (frame, font, padding)
|
||
|
|
- **Use `.interactive()`** only on elements that respond to user interaction (buttons, toggleable items)
|
||
|
|
- **Choose spacing carefully** in containers to control when glass effects merge
|
||
|
|
- **Use `withAnimation`** when changing view hierarchies to enable smooth morphing transitions
|
||
|
|
- **Test across appearances** — light mode, dark mode, and accented/tinted modes
|
||
|
|
- **Ensure accessibility contrast** — text on glass must remain readable
|
||
|
|
|
||
|
|
## Anti-Patterns to Avoid
|
||
|
|
|
||
|
|
- Using multiple standalone `.glassEffect()` views without a GlassEffectContainer
|
||
|
|
- Nesting too many glass effects — degrades performance and visual clarity
|
||
|
|
- Applying glass to every view — reserve for interactive elements, toolbars, and cards
|
||
|
|
- Forgetting `clipsToBounds = true` in UIKit when using corner radii
|
||
|
|
- Ignoring accented rendering mode in widgets — breaks tinted Home Screen appearance
|
||
|
|
- Using opaque backgrounds behind glass — defeats the translucency effect
|
||
|
|
|
||
|
|
## When to Use
|
||
|
|
|
||
|
|
- Navigation bars, toolbars, and tab bars with the new iOS 26 design
|
||
|
|
- Floating action buttons and card-style containers
|
||
|
|
- Interactive controls that need visual depth and touch feedback
|
||
|
|
- Widgets that should integrate with the system's Liquid Glass appearance
|
||
|
|
- Morphing transitions between related UI states
|