← 所有文章

SwiftUI中的Liquid Glass:从Return在iOS 26上的发布中提炼的三种模式

Apple的Liquid Glass是一行SwiftUI API:.glassEffect()1 Return是我开发的冥想计时器,在iOS、macOS和tvOS上共使用了它9次。2 其中有一处用法将该修饰符应用于自定义Shape,把计时器数字本身逐字形地变为液态玻璃。

iPhone上的Return展示Liquid Glass计时器数字折射火焰主题背景,下方是时长选择器的玻璃HUD

有趣的问题在于:当你超越这一行代码时会发生什么。Apple的人机界面指南规定了严格的分层规则:Liquid Glass属于功能层(控件、导航、瞬态UI),绝不属于内容层3 Return的9次用法中大多数都是教科书式的功能层应用:选择器、按钮、控件条、暂停状态徽章。真正有意思的是其中3处在不破坏规则的前提下灵活变通的用法。

本文将详细介绍我交付的三种模式、它们所遵循的规则、我遇到的陷阱,以及一个我刻意没有使用的API接口。

TL;DR

  • iOS 26将Liquid Glass作为.glassEffect(_:in:)提供。默认变体是.regular,默认形状是Capsule1
  • Return使用了三种超越一行代码的模式:在自定义Shape上应用玻璃效果(通过Core Text字形路径渲染计时器文本)、镜像模式(通过翻转并遮罩的副本在下方形成反射),以及功能层HUD叠加层。
  • Apple HIG规则:Liquid Glass用于功能层,标准材质用于内容层。3
  • 我刻意没有使用GlassEffectContainer。变形API在Return中没有用例(没有玻璃元素需要变形为另一个),并且我没有对渲染性能差距进行基准测试;这是一个未经测量的权衡,而非一项推荐做法。1
  • 陷阱:在纯色背景上的玻璃效果看起来扁平;无抖动的数字渲染需要固定宽度的单元格;tvOS的HStack会忽略布局方向环境值;变形动画必须遵守减少动画偏好。

一行API与分层规则

Apple为Liquid Glass提供的接口面积很小,作为重要的设计支柱在WWDC 2025上推出:113

Text("Hello, World!")
    .font(.title)
    .padding()
    .glassEffect()                              // default: .regular variant, Capsule shape

Text("Hello, World!")
    .glassEffect(in: .rect(cornerRadius: 16))   // custom shape

Text("Hello, World!")
    .glassEffect(.regular.tint(.orange).interactive())  // tint + touch reactivity

三个旋钮:变体(.regular.clear)、形状(任何Shape),以及GlassEffectStyle链(着色、可交互)。这就是单个视图的全部API。多视图渲染由独立的GlassEffectContainer处理,稍后会讲到。

HIG比API更严格。Apple的人机界面指南为每一个iOS 26+界面定义了两个层级:3

  1. 内容层:用户正在消费的文档、列表、照片或媒体。这里使用标准材质(已有的.regularMaterial.thinMaterial等)。
  2. 功能层:控件、导航、标签栏、侧边栏、瞬态叠加层。这里使用Liquid Glass

Apple的具体说明:“不要在内容层使用Liquid Glass。Liquid Glass在交互元素和内容之间提供清晰区分时效果最佳,将其包含在内容层会导致不必要的复杂性和混乱的视觉层次。”3

这条规则听起来很有限制性,直到你将其映射到一个真实的应用上。Return是一个冥想计时器。它的内容层是呼吸图像和在所有元素背后循环播放的视频。它的功能层是时长选择器、开始/暂停/停止按钮组、次级设置按钮行,以及(在tvOS上)暂停状态徽章。Return的9处玻璃用法中有8处都是教科书式的功能层应用:iOS和macOS代码路径上的三个时长选择器变体、开始/暂停切换按钮、停止按钮、设置按钮行、tvOS暂停指示器,以及一个瞬态控件叠加层。4

第9处是刻意为之的边界情况(在计时器数字本身上使用Liquid Glass),下一节会详细说明。

模式1:自定义Shape上的玻璃效果

Return中的计时器数字并不是绘制在玻璃背景之上的文本。玻璃就是文本本身。.glassEffect(.clear, in:)接受任何Shape9Shape是一个产生Path的协议。10 因此诀窍是:使用Core Text将计时器字符串转换为字形路径,11 然后将该路径作为Shape传递给.glassEffect56

import SwiftUI
@preconcurrency import CoreText

struct GlassTextShape: Shape {
    let text: String
    let font: CTFont

    func path(in rect: CGRect) -> Path {
        guard !text.isEmpty else { return Path() }
        let combinedPath = CGMutablePath()

        let attrString = NSAttributedString(string: text, attributes: [.font: font])
        let line = CTLineCreateWithAttributedString(attrString)
        guard let runs = CTLineGetGlyphRuns(line) as? [CTRun], !runs.isEmpty else {
            return Path()
        }

        for run in runs {
            let glyphCount = CTRunGetGlyphCount(run)
            guard glyphCount > 0 else { continue }
            var glyphs = [CGGlyph](repeating: 0, count: glyphCount)
            var positions = [CGPoint](repeating: .zero, count: glyphCount)
            let range = CFRange(location: 0, length: glyphCount)
            CTRunGetGlyphs(run, range, &glyphs)
            CTRunGetPositions(run, range, &positions)

            for i in 0..<glyphCount {
                guard let glyphPath = CTFontCreatePathForGlyph(font, glyphs[i], nil) else { continue }
                let transform = CGAffineTransform(translationX: positions[i].x, y: positions[i].y)
                combinedPath.addPath(glyphPath, transform: transform)
            }
        }

        // Core Text y-axis is flipped vs SwiftUI; flip then re-bound and center.
        var swiftPath = Path(combinedPath).applying(CGAffineTransform(scaleX: 1, y: -1))
        let flippedBounds = swiftPath.boundingRect
        let offsetX = rect.midX - flippedBounds.midX
        let offsetY = rect.midY - flippedBounds.midY
        return swiftPath.applying(CGAffineTransform(translationX: offsetX, y: offsetY))
    }
}

这是来自Return/Return/GlassTextShape.swift的真实生产代码。5 path(in:)函数使用Core Text对字符串进行排版,遍历每个CTRun,提取每个字形的CGPath,并将它们合并为一个CGMutablePath。合并之后还有两个不太显而易见的步骤:Core Text的坐标系将原点放在左下角,而SwiftUI的Path将其放在左上角,所以路径必须通过CGAffineTransform(scaleX: 1, y: -1)翻转。然后翻转后路径的boundingRect具有负y值,因此需要平移以重新将其居中于SwiftUI传递给Shape的rect内。跳过任何一个变换都会导致字形上下颠倒或显示在屏幕外。

应用这段代码只需一行:

Rectangle()
    .fill(.clear)
    .glassEffect(.clear, in: textShape)
    .frame(width: cellWidth, height: cellHeight)

透明的Rectangle是一个命中目标占位符;实际视觉效果由textShape产生的形状决定。使用字形路径形状,Liquid Glass材质只填充字形轮廓。结果是:计时器的每个数字都是独立的液态玻璃形态,折射其后方运行的任何动画。6

HIG的微妙之处。 Apple陈述的规则是Liquid Glass用于功能层,标准材质用于内容层,并有一个明确的例外:内容层中的瞬态交互控件(滑块、开关)激活时可以采用Liquid Glass。3 Return中的计时器数字是状态显示,而非控件:它们每秒从Timer.publish(every: 1, ...)更新一次,没有点击手势(位于其下方的开始/暂停按钮才是切换状态的元素)。因此在它们上面放Liquid Glass是一个刻意为之的边界情况,从意图上更接近”瞬态交互控件”而非字面上的可交互性,因为这些数字是用户整个会话期间注视的视觉焦点。我是在变通规则,而非破坏它。一位严格阅读HIG的审阅者可能会争辩说这里应该使用标准材质;我则认为计时器是一个时间流逝控件表面,与进度指示器属于同一类别。Apple的文档没有直接对此案例作出裁定。

为什么使用自定义Shape而非Text + 背景。 在玻璃背景上方渲染的Text读起来是玻璃之上的文本。而作为玻璃本身渲染的Text读起来则是另一种视觉类别。用户感知这些数字是功能性前景,具体而言是一种存在的目的是被透视而非被注视的瞬态元素。

模式2:镜像模式

Return在计时器下方显示其反射,逐渐淡出。真实生产代码:6

Mac上的Return展示作为Liquid Glass HUD叠加层的时长选择器,上方可见计时器文本的玻璃处理

VStack(spacing: 0) {
    GlassTimerText(text: displayTime, fontSize: fontSize)
        .accessibilityLabel("Time remaining: \(accessibleDescription)")

    if showReflection {
        GlassTimerText(text: displayTime, fontSize: fontSize)
            .scaleEffect(x: 1, y: -1)
            .mask(
                LinearGradient(
                    stops: [
                        .init(color: .white.opacity(0.2), location: 0),
                        .init(color: .clear, location: 0.6)
                    ],
                    startPoint: .top,
                    endPoint: .bottom
                )
            )
            .offset(y: -8)
            .accessibilityHidden(true)
    }
}

镜像由三个变换组成,全部是标准SwiftUI原语:14

  1. scaleEffect(x: 1, y: -1)将第二个副本上下翻转。
  2. .mask(LinearGradient(...))将反射从顶部20%不透明度淡化到60%处完全透明。
  3. .offset(y: -8)将反射上拉8点,使其紧靠原始内容而不留下可见的接缝。

反射上的.accessibilityHidden(true)修饰符至关重要。VoiceOver不应两次播报镜像后的时间;原始的accessibilityLabelaccessibilityAddTraits(.updatesFrequently)已经附加到上方的主GlassTimerText实例上,反射纯粹是装饰性的。

为什么这特别适用于Liquid Glass。 反射继承了GlassTimerText的玻璃材质。原始内容所在的任何背景(呼吸圆形渐变、视频、着色场景)都会通过两个副本折射。镜像不需要任何玻璃专用代码;玻璃材质免费处理折射。整个效果只需三个修饰符和一个渐变。

无障碍成本。 启用减少动画的用户仍然会看到镜像,但玻璃材质在时间更新之间的动画通过@Environment(\.accessibilityReduceMotion)在其他位置被抑制。7 反射本身是静态的;只有数字过渡之间的变形会产生动画。

模式3:用于瞬态控件的玻璃HUD叠加层

Return中剩余的8处玻璃用法都是教科书式的功能层应用。4 每一处都遵循相同的模式:

Apple Watch上的Return在小画布尺寸下展示开始/暂停控件上的功能层Liquid Glass

durationPicker
    .frame(height: 50)
    .frame(maxWidth: 320)
    .glassEffect()
    .padding(.horizontal, 20)
    .transition(.opacity.combined(with: .scale(scale: 0.95)))

.transition(.opacity.combined(with: .scale(scale: 0.95)))是关键所在。Liquid Glass用于瞬态控件,只有在控件瞬态出现时才感觉对路。永久停留在屏幕上的静态玻璃HUD看起来像是装饰元素。当用户点击时淡入并缩放、移开视线时淡出的玻璃HUD则像是一个瞬时的控件表面。

Apple关于glassEffect的文档隐含地指出了这一点:该修饰符”捕获内容并发送到容器进行渲染”,并”实时响应触摸和指针交互”。1 动画钩子不在API中,但渲染管线假设玻璃元素会移动。静态玻璃元素则会失去这种功能可见性。

Return将该模式用于时长选择器(用户点击时向上滑出)、开始/暂停切换按钮(始终可见但按下时缩放)、停止按钮(仅在会话进行中可见)、设置按钮行(位于时长选择器下方的水平控件条),以及tvOS暂停状态徽章(仅在Apple TV上会话暂停时可见)。所有五个上下文都遵守HIG的功能层规则。3

Apple TV上的Return展示为10英尺界面缩放后的Liquid Glass处理

GlassEffectContainer的问题

Apple建议每当应用在多个视图上使用.glassEffect()时使用GlassEffectContainer,理由有二:更好的渲染性能(玻璃效果会被批处理),以及在过渡期间将形状变形为彼此的能力。1

我没有使用它。理由是特定于应用的,而不是对Apple指导的反驳。Return有9个玻璃视图,没有一个需要在彼此之间变形。46 时长选择器从不动画化为开始按钮。计时器文本从不动画化为设置按钮行。每个玻璃元素都是独立的。变形API没有可触发的用例,而容器的间距规则会约束今天无需协调的布局。

渲染性能的论点我无法在没有测量的情况下完全反驳。Apple的文档警告说,容器之外”过多”的玻璃效果会降低性能。1 Return的9个视图从不会同时出现在屏幕上(时长选择器只在菜单状态下出现,停止按钮只在会话中暂停时出现)。在任何给定帧上,我都数到3到4个可见的玻璃元素,这在我测试过的iOS、iPadOS、macOS、watchOS和tvOS的每台设备上都很流畅,但我没有运行过instruments追踪来对比容器包装与仅修饰符的方案。所以诚实的说法是:Return跳过GlassEffectContainer是基于观察到的良好用户体验,而非测量后的性能等价。

我从中得出的规则是:GlassEffectContainer适用于多个玻璃元素同时可见且动画化的应用。 Apple的示例是使用glassEffectUnion(id:namespace:)进行符号集渲染:四个天气符号作为一个单元流畅地合并和分离。1 那是一个教科书式的用例。如果Return未来的某个功能需要玻璃元素变形或共享容器的间距规则,那时再添加容器才是正确的工具。对于今天的应用而言,我尚未遇到此情况。

我遇到的陷阱

来自生产环境的三个真实Bug:

玻璃数字抖动。 SF Pro Rounded在比例渲染中具有可变宽度的数字。计时器倒计时时,显示字符串的长度会变化,周围的HStack每秒重排一次,让整个计时器抖动。修复方法是:为每个字符使用固定宽度的单元格。每个数字的cellWidthfontSize * 0.6,每个冒号为fontSize * 0.3HStack变成稳定的网格。6

HStack(spacing: 0) {
    ForEach(Array(text.enumerated()), id: \.offset) { _, char in
        let isColon = char == ":"
        let cellWidth = isColon ? colonCellWidth : digitCellWidth
        GlassDigitCell(character: String(char), font: ctFont,
                       cellWidth: cellWidth, cellHeight: cellHeight)
    }
}

这些单元格不是Apple标准的;它们是在固定字体小尺寸下针对比例宽度渲染的变通方案。Apple的SF Pro Rounded配合.monospacedDigit()会在Text上解决同样的问题,但该修饰符不可用于基于自定义Shape的玻璃渲染器。固定单元格布局是替代方案。

tvOS布局方向覆盖。 同样的GlassTimerText运行在iOS、iPadOS、macOS和tvOS上。特别是在tvOS上,即使iOS版本遵守了环境内的覆盖,HStack仍在右到左语言环境下被镜像。修复方法是:通过环境值以及显式的flipsForRightToLeftLayoutDirection(false)修饰符同时固定布局方向,直接应用于数字单元格的HStack(父级VStack单独应用环境覆盖以便反射副本继承它):6

HStack(spacing: 0) { ... }
    .flipsForRightToLeftLayoutDirection(false)
    .environment(\.layoutDirection, .leftToRight)

原因是:tvOS的HStack在某些版本中似乎忽略环境级别的覆盖,而flipsForRightToLeftLayoutDirection(false)是一个被更可靠地遵守的显式不镜像契约。12 双保险。

数字变形上的减少动画。 Liquid Glass默认在显示字符串之间动画化变形过渡。启用accessibilityReduceMotion的用户看到的变形是闪烁的。修复方法:6

.animation(reduceMotion ? nil : .easeInOut(duration: 0.15), value: displayTime)

动画修饰符读取@Environment(\.accessibilityReduceMotion)并在减少动画开启时完全禁用过渡。Apple的无障碍指南是明确的:任何装饰性动画都必须尊重用户的动画偏好。7

何时不要使用Liquid Glass

拒绝使用也是设计的一部分。

不要在内容层使用Liquid Glass。 Apple的HIG是明确的,忽略该规则会产生混乱的层次结构:用户无法分辨什么是可交互的、什么是内容。3 如果一个玻璃效果装饰着列表行或照片卡片,那么设计就在与平台对抗。

不要在纯色背景上使用玻璃效果。 Liquid Glass折射其后方的内容。如果”后方的内容”是单一纯色,折射就没有可弯曲的对象,结果看起来就是一个扁平的着色矩形。要么将玻璃放在多变的内容(渐变、图像、视频)之上,要么完全不使用玻璃。Return的计时器屏幕通过VideoBackgroundView将基于主题的封面图像和循环视频作为背景运行,4 正是为了让其上方的玻璃元素始终有纹理可以折射。

对高频内容上的玻璃效果保持谨慎。 玻璃材质渲染受GPU约束,玻璃形状变化之间的默认变形动画本身就是一种动画。每秒更新一次的计时器在我的测试中没问题;60Hz的波形或音频可视化器尚未经过验证,可能会与变形动画冲突。我没有对上限进行基准测试;将其视为启发式经验,而非测量后的阈值。Apple的文档没有公布这一阈值。

不要在没有测试减少动画的情况下交付玻璃效果。 每个玻璃动画都应基于accessibilityReduceMotion进行控制。7 玻璃形状之间的默认变形是一种动态效果,而不仅仅是淡入淡出。

Liquid Glass对iOS 26+应用的意义

论点很简单。Liquid Glass只有在应用已经遵守HIG分层规则时才是一行API。 一个将控件放在功能层、内容放在内容层的SwiftUI应用,可以通过.glassEffect()修饰符采用Liquid Glass,默认就感觉原生。

混合两个层级的应用(列表行内的控件、被视为内容的导航栏、照片卡片上的装饰元素)采用Liquid Glass会感觉不对劲。材质是正确的,但其下的架构不是。

自定义Shape模式(模式1)干净地扩展了规则。任何在功能上是控件的东西都可以采用Liquid Glass,即使它在传统意义上看起来不像”控件”。计时器是控件,电平表是控件,进度指示器是控件。在它们每一个上使用Liquid Glass都符合规范。

将本文与我之前关于通过App Intents和通过MCP服务器交付同一应用数据层的文章配合阅读。视觉层是同一栈的第三个表面:用于系统AI的类型化实体、用于跨LLM代理的文件格式,以及用于设备前用户的Liquid Glass。8

FAQ

我可以在非iOS 26平台上使用.glassEffect()吗?

.glassEffect()修饰符仅限iOS 26+、iPadOS 26+、macOS 26+、watchOS 26+、tvOS 26+、visionOS 26+。26之前的平台有.background(.regularMaterial)等类似方案,可产生磨砂玻璃效果,但没有新的Liquid Glass折射效果。1

GlassEffectContainer会改变视觉效果吗?

容器包装的玻璃元素在其间距规则导致重叠时可以融合形状。没有容器时,每个.glassEffect()都是独立的。对于玻璃元素应在动画期间流畅合并的应用,GlassEffectContainer是正确的工具。对于每个玻璃元素都保持独立的应用,容器是负担。1

为什么不直接对Text使用.foregroundStyle(.thinMaterial)

thinMaterial是标准材质,而非Liquid Glass。视觉效果是磨砂玻璃叠加层,而不是Liquid Glass的折射式光线弯曲效果。3 对于希望特别看起来像新材质的文本,.glassEffect(.clear, in: customShape)才是受支持的路径。

我如何为营销目的捕获Liquid Glass截图?

玻璃效果在运行时由GPU渲染,因此截图是从已经应用效果的模拟器或设备上拍摄的。Apple官方的Liquid Glass参考图像来自其HIG文档页面和WWDC 2025会话。3

GlassTextShape适用于任意文本还是仅适用于数字?

任何Core Text可以排版的字符串都可以使用。Return将其用于数字和冒号,但相同的Shape适用于字母、符号、表情符号(搭配合适的字体)或混合字符串。性能受字形数量限制;将一段长段落渲染为玻璃将代价高昂,但六个字符的计时器则微不足道。


三种模式、一条规则,以及一个我刻意跳过的API。Liquid Glass是iOS 26+应用的第三个表面,位于类型化实体和共享文件格式之上。一行API是真实存在的。其下的HIG规则才是让这一行代码生效的关键。

参考资料


  1. Apple Developer,《将Liquid Glass应用于自定义视图》glassEffect(_:in:)修饰符、GlassEffectContainerglassEffectUnion(id:namespace:)glassEffectID(_:in:)GlassEffectTransition的文档。默认变体.regular,默认形状Capsule。 

  2. 作者的Return,一款于2026年4月21日在App Store发布的冥想计时器应用,可用于iPhone、iPad、Mac、Apple Watch和Apple TV。在iOS 26+ / macOS 26+上使用SwiftUI、SwiftData和HealthKit。 

  3. Apple Developer,《材质》人机界面指南。定义了Liquid Glass的功能层与内容层规则:“不要在内容层使用Liquid Glass。” 列出了regular和clear变体及其预期用途。 

  4. 生产代码位于Return/Return/ContentView.swift(七处.glassEffect()调用点)、Return/Return/GlassTimerText.swift(一处在GlassDigitCell上的调用点),以及Return/ReturnTV/TVContentView.swift(一处在tvOS”Paused”指示器上的调用点)。共计九处。另外还有Return/Return/VideoBackgroundView.swift,渲染基于主题的封面图像和循环视频,玻璃元素通过它们折射。 

  5. 生产代码位于Return/Return/GlassTextShape.swift。Core Text之上的Shape合规包装。创建于2025年11月26日,包含在已发布的App Store v1.0中。 

  6. 生产代码位于Return/Return/GlassTimerText.swiftGlassDigitCellGlassTimerTextGlassTimerDisplay视图。实现固定宽度的单元格布局、镜像反射和减少动画的控制。 

  7. Apple Developer,accessibilityReduceMotion环境值。应用必须遵守用户的动画偏好;Liquid Glass上的默认变形动画应基于该值进行控制。 

  8. 作者的分析见《App Intents是Apple为你的应用提供的新API》《两个代理生态系统,一个购物清单》。三表面模型:用于Apple Intelligence的App Intents、用于跨LLM代理的MCP,以及用于设备前用户的Liquid Glass。 

  9. Apple Developer,View上的glassEffect(_:in:isEnabled:)in:参数接受任何Shape合规类型。默认形状是Capsule。 

  10. Apple Developer,Shape协议Shape是任何为给定矩形生成Path的类型。自定义形状可以包装任意CGPath数据。 

  11. Apple Developer,《Core Text编程指南》CTLineCreateWithAttributedString。Core Text是较低级别的文本引擎,用于将带属性字符串排版为字形运行并提取每个字形的路径。 

  12. Apple Developer,flipsForRightToLeftLayoutDirection(_:)。无论周围的\.layoutDirection环境值如何,都显式地覆盖View上的RTL镜像。 

  13. Apple,Apple Newsroom《WWDC 2025亮点》。Liquid Glass作为iOS 26、iPadOS 26、macOS 26、watchOS 26、tvOS 26和visionOS 26的统一设计材质宣布发布。会话:《认识Liquid Glass》(WWDC 2025)《使用Liquid Glass构建SwiftUI应用》。 

  14. Apple Developer,LinearGradientscaleEffect(x:y:anchor:)mask(_:)。标准SwiftUI原语,自iOS 13起均可使用。 

相关文章

iOS 26 上的 HealthKit + SwiftUI:来自两款已上架应用的授权流程、样本类型与跨平台模式

来自 Water(饮水追踪,HKQuantitySample)和 Return(正念会话,HKCategorySample)的真实生产模式。权限 UX、async 封装、watchOS 变体,以及需要避开的陷阱。

5 分钟阅读

SwiftUI Layout 协议:从 sizeThatFits 到 placeSubviews 构建自定义布局

iOS 16 的 Layout 协议让应用可通过两个方法构建自定义布局。sizeThatFits、placeSubviews、proposal 契约、布局值与缓存。

4 分钟阅读

审美是基础设施

当智能体生成越来越多最终交付的产出时,质量上限取决于你将审美判断编码进系统的能力。审美在变得可查询时才能规模化。

1 分钟阅读