iOS 26 的 Liquid Glass:如何让 SwiftUI 应用适配苹果的新设计语言#
WWDC 2025 上 Liquid Glass 公布的那一刻,大多数 iOS 开发者的第一反应不是"这东西怎么实现",而是"接下来又要重做多少个界面"。这是个合理的疑问。Liquid Glass 并不是像 iOS 7 那次扁平化那样的表层换皮,而是一种拥有自身物理特性的材质:它会折射光线,对设备运动和触摸做出反应,能与相邻元素融合,还能在不同状态之间发生形变(morph)。SwiftUI 为此提供了一整套专属 API,如果只是机械地把它套用到旧的界面上,应用放在系统原生界面旁边就会显得格格不入。
笔者已经把包括 MeteoHealth 在内的几款自有应用迁移到了 Liquid Glass,这篇文章整理的是真正有用的经验:截至 2026 年的最新 API(包含 iOS 26.1、26.2 的调整)、迁移自定义界面时反复出现的错误,以及用户打开 Reduce Transparency 之后应用会发生什么。
Liquid Glass 到底是什么——不只是又一种模糊效果#
在 iOS 26 之前,我们有的是"材质"——.ultraThinMaterial、.regularMaterial 等一系列 Material,本质是可调透明度的静态模糊。Liquid Glass 不是材质的进化版,而是一个独立的系统层,具备以下特性:
- 折射并反射下方的内容,而不只是简单地模糊它
- 响应触摸和指针操作——按下时元素会轻微回弹并高亮
- 相邻的 glass 元素会互相融合,view 层级发生变化时形状之间还会发生 形变过渡
- 自动适配下方的内容——浅色背景下边缘对比度更高,深色背景下则显得更透明
苹果给出的核心设计原则是:Liquid Glass 是功能层(导航、工具栏、控件、临时性浮层)专用的材质,而不属于内容层。文章卡片、图库中的照片、消息列表——这些都是内容,即便技术上可行,把它们做成玻璃也不是好主意。第二条原则是不要"玻璃叠玻璃":如果某个元素本身已经位于系统的 navigation bar 或 sheet 之上,再叠加一层 glassEffect 只会产生浑浊的观感,而不是预期中的层次感。
新 API:glassEffect、Glass 以及修饰符的顺序#
最基础的入口是 .glassEffect() 修饰符。不带参数使用时,它会把 view 包裹在 .regular 变体的 Capsule 形状里:
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect()你可以显式指定形状(.rect(cornerRadius:)、.circle、.capsule),材质的行为则通过 Glass 结构体配置:.regular 是基础玻璃效果,.tint(Color) 为更重要的元素添加色调强调,.interactive() 则开启点按时的弹性反馈:
struct WeatherSummaryCard: View {
let temperature: String
let condition: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(condition)
.font(.headline)
Text(temperature)
.font(.system(size: 34, weight: .semibold, design: .rounded))
}
.padding(20)
.frame(maxWidth: .infinity, alignment: .leading)
.modifier(AdaptiveGlassBackground(cornerRadius: 24))
}
}
/// Applies Liquid Glass on iOS 26+, falls back to Material on older systems.
/// Note the order: layout modifiers (padding, frame) come first,
/// the glass effect is applied last, on top of the finished layout.
struct AdaptiveGlassBackground: ViewModifier {
let cornerRadius: CGFloat
func body(content: Content) -> some View {
if #available(iOS 26.0, *) {
content.glassEffect(
.regular.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
content.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
}
}
}这里修饰符的顺序至关重要。glassEffect 必须放在决定布局和外观的修饰符(padding、frame、font)之后,而不是之前——材质需要测量 view 最终的边界,因此必须在布局已经确定之后再应用。把 .glassEffect() 写在 .padding() 之前,是"玻璃效果在错误的边界被裁切"这个典型问题的常见成因。
GlassEffectContainer、形变过渡与 glassEffectID:动态状态管理#
一旦有多个 glass 元素彼此靠近排列——比如一个快捷操作栏——就需要把它们包在 GlassEffectContainer 里。这个容器会建立一个共享的采样区域,让相邻元素在视觉上真正融合,而不只是简单叠放:
enum QuickAction: String, CaseIterable, Identifiable {
case refresh, share, favorite
var id: String { rawValue }
var symbolName: String {
switch self {
case .refresh: return "arrow.clockwise"
case .share: return "square.and.arrow.up"
case .favorite: return "heart"
}
}
}
struct QuickActionsBar: View {
@State private var isExpanded = false
@Namespace private var glassNamespace
var body: some View {
GlassEffectContainer(spacing: 24) {
HStack(spacing: 24) {
Button {
withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
isExpanded.toggle()
}
} label: {
Image(systemName: isExpanded ? "xmark" : "plus")
.frame(width: 56, height: 56)
}
.buttonStyle(.glass)
.glassEffectID("toggle", in: glassNamespace)
if isExpanded {
ForEach(QuickAction.allCases) { action in
Button {
// handle action
} label: {
Image(systemName: action.symbolName)
.frame(width: 56, height: 56)
}
.buttonStyle(.glass)
.glassEffectID(action.id, in: glassNamespace)
.glassEffectUnion(id: "expanded", namespace: glassNamespace)
}
}
}
}
}
}这里有两个细节值得注意。第一,GlassEffectContainer 的 spacing 决定了相邻元素需要多近才会开始视觉融合——数值越小,view 之间就需要靠得越近。第二,glassEffectID 配合 @Namespace 正是让按钮的出现与消失呈现平滑形变、而不是生硬交叉淡入淡出的关键——如果不用 withAnimation 包裹 isExpanded 的变化,这个效果根本不会触发。glassEffectUnion 还能把在共享 HStack 之外创建的元素——例如动态的 ForEach 中——归并为同一个 glass 表面。
低于 iOS 26 的降级方案:一个应用里的两套设计语言#
如果应用还有低于 iOS 26 版本的用户——绝大多数真实产品在接下来一两年里都会如此——降级方案就不是可选项,而是适配工作的必要组成部分。正确的做法不是维护两套并行的 view 树,而是像上面 AdaptiveGlassBackground 那样,把判断封装进一个由 #available 控制的修饰符里:iOS 26 及以上使用 .glassEffect(...),更旧的系统使用 .background(.ultraThinMaterial, in:),并保持相同的形状(RoundedRectangle 对应 .rect(cornerRadius:)),这样卡片的几何形态不会随 iOS 版本发生跳动。
这一步常见的失误是忘记 .glassEffect() 本身带有 isEnabled 参数,它比在 view 内部反复写 if #available 更方便:可以只维护一套 view 实现,通过在树的更上层计算一次的标志位来切换材质。
迁移自定义界面时常见的错误#
在迁移过几款应用之后,会发现同样的错误反复出现:
- 玻璃叠玻璃。 在已经带有玻璃工具栏的系统
sheet或NavigationStack里,给自定义卡片加上glassEffect,结果会变成一团不透明的灰色,对比度和文字可读性都会下降。 - 在内容层使用 glass。 文章封面、头像照片、播放器背景——这些都是内容而非功能元素;试图用玻璃"美化"内容,通常只会让界面更难读,而不是显得更高级。
- 多个独立 glass 元素却没有容器。 每个图标各自带上
glassEffect(),却没有用GlassEffectContainer包裹——渲染成本更高,而本该出现的视觉融合却始终没有发生。 - 修饰符顺序错误。
glassEffect写在padding或frame之前——材质会按照尚未完成的布局去测量边界。 - 忽视 Reduce Transparency。 只判断
#available(iOS 26, *)而不做其他处理的代码,忽略了部分用户是主动降低了界面透明度的。
无障碍:Reduce Transparency、Tinted 模式,以及不能跳过的部分#
从 iOS 26.1 开始,苹果给了用户直接控制 Liquid Glass 的能力:辅助功能 → 显示与文字大小 中的 Reduce Transparency 开关会提高材质的不透明度,设置 → 显示与亮度 → Liquid Glass 中也新增了在 "Clear" 与 "Tinted" 之间切换的选项——后者通过轻微加深来提升对比度。iOS 26.2 又为锁屏时钟增加了一个透明度滑块。
对开发者来说,这意味着当 accessibilityReduceTransparency 开启时,并不需要手动禁用 glassEffect——系统本身就会自动提高材质的不透明度。真正需要开发者自己处理的,是在决定是否添加 .interactive() 时考虑这个 environment 值,因为弹性动画属于另一个独立的无障碍问题,和透明度并不完全等同:
struct AccessibleGlassSurface<Content: View>: View {
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
let cornerRadius: CGFloat
@ViewBuilder var content: Content
var body: some View {
if #available(iOS 26.0, *) {
content
// Don't disable glassEffect manually when Reduce Transparency
// is on — iOS already increases frosting for the .regular
// variant. We only drop the extra .interactive() bounce,
// since motion is a separate accessibility concern.
.glassEffect(
reduceTransparency ? .regular : .regular.interactive(),
in: .rect(cornerRadius: cornerRadius)
)
} else {
content.background(
.ultraThinMaterial,
in: RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
}
}
}不要只在真机上反复切换系统设置来测试,预览环境同样适用:在 #Preview 中使用 .environment(\.accessibilityReduceTransparency, true) 能省下不少来回切换的时间。
API 对照表:适配时需要替换什么#
| UI 元素 | iOS 26 之前 | iOS 26(Liquid Glass) |
|---|---|---|
| 卡片 / 面板 | .background(.ultraThinMaterial, in: RoundedRectangle(...)) | .glassEffect(.regular, in: .rect(cornerRadius:)) |
| 操作按钮 | 手写模糊和阴影的自定义 ZStack | .buttonStyle(.glass) / .buttonStyle(.glassProminent) |
| 相邻图标组 | 每个元素各自使用独立材质 | GlassEffectContainer + glassEffectUnion |
| 元素的出现 / 消失 | 不感知相邻元素的普通 .transition | 用于形变过渡的 glassEffectID + @Namespace |
| Toolbar / tab bar accessory | 自定义背景,手动分组 | 自动生成的 glass 表面 + ToolbarSpacer |
实践中的落地:MeteoHealth 与适配检查清单#
在 MeteoHealth 中,真正被改造成玻璃效果的只有功能性元素:主屏幕上的快捷操作栏、悬浮在健康趋势图之上的控件,以及展示当前天气摘要的 tab bar accessory——都是位于内容"之上"、而非内容本身的部分。预测卡片和历史指标图表则保留了普通背景:真实用户测试很快就证明,在密集的数值数据上叠加玻璃效果,只会降低可读性,而不会让界面显得更高级。
发布前的最终检查清单:
- glass 效果只应用于功能层——导航、工具栏、临时性控件
- 不存在"玻璃叠玻璃"的情况(已在真实的 sheet / NavigationStack 中验证)
- 相邻的 glass 元素已用
GlassEffectContainer包裹 -
glassEffect位于布局修饰符之后,而非之前 - 具备
#available(iOS 26, *)降级到Material的方案,且形状几何保持一致 - 已在预览和真机上验证
accessibilityReduceTransparency下的表现 - 已测试 设置 → 显示与亮度 → Liquid Glass 中的 Tinted 模式
- 状态之间的形变过渡已用
withAnimation包裹
Liquid Glass 是苹果少有的一次真正推出材质级更新的案例,它不是表层换皮,而是拥有真实物理特性和自身组合规则的材质。把旧的 Material 模糊效果原样替换成 glassEffect,在只有一个 glass 元素的画面里是行得通的,可一旦出现两个 glass 元素——或者一个 glass 元素和一个系统组件——彼此相邻,问题就不再是 API 语法层面的事了,而变成了应用里哪一层是功能层、哪一层是内容层。而这个划分,远比语法本身更能决定适配之后的界面是否真正"原生"。
相关链接:



