Liquid Glass 自定义视图应用教程 发布于 · 2026-09-07 · # 默认分类 # Liquid Glass 自定义视图应用教程 > 基于 Apple 官方文档 [Applying Liquid Glass to custom views](https://developer.apple.com/documentation/swiftui/applying-liquid-glass-to-custom-views) 整理与扩展。 > > 适用平台:macOS 26+、iOS 26+、iPadOS 26+、visionOS 26+ 等支持 Liquid Glass 的系统。需使用 Xcode 26 及 Swift 6 编译。 ## 目录 - [教程概览](#教程概览) - [初级篇:给单个视图加上 Liquid Glass](#初级篇给单个视图加上-liquid-glass) - [1.1 最基础的 glassEffect](#11-最基础的-glasseffect) - [1.2 指定形状与圆角](#12-指定形状与圆角) - [1.3 用 tint 表达语义](#13-用-tint-表达语义) - [1.4 用 interactive 让玻璃"活"起来](#14-用-interactive-让玻璃活起来) - [1.5 用 .glass 按钮样式](#15-用-glass-按钮样式) - [初级篇小结](#初级篇小结) - [中级篇:用容器组合多个玻璃视图](#中级篇用容器组合多个玻璃视图) - [2.1 为什么需要 GlassEffectContainer](#21-为什么需要-glasseffectcontainer) - [2.2 spacing 如何控制"融合"距离](#22-spacing-如何控制融合距离) - [2.3 用 glassEffectUnion 合并静止形状](#23-用-glasseffectunion-合并静止形状) - [2.4 动态生成视图时的合并](#24-动态生成视图时的合并) - [中级篇小结](#中级篇小结) - [高级篇:形变过渡、自定义转场与性能](#高级篇形变过渡自定义转场与性能) - [3.1 glassEffectID 与命名空间](#31-glasseffectid-与命名空间) - [3.2 GlassEffectTransition:matchedGeometry 与 materialize](#32-glasseffecttransitionmatchedgeometry-与-materialize) - [3.3 完整的展开/收起形变示例](#33-完整的展开收起形变示例) - [3.4 性能优化要点](#34-性能优化要点) - [高级篇小结](#高级篇小结) - [引申应用场景案例](#引申应用场景案例) - [案例一:浮动媒体播放控件条](#案例一浮动媒体播放控件条) - [案例二:可展开的浮动工具盘](#案例二可展开的浮动工具盘) - [案例三:状态徽标集群](#案例三状态徽标集群) - [附录:API 速查表](#附录api-速查表) --- ## 教程概览 Liquid Glass 是 Apple 平台全新的动态材质:它把玻璃的光学属性(模糊、折射、反光)与一种"流动感"结合在一起,并会实时响应触摸与指针交互。SwiftUI 的标准控件已经默认采用 Liquid Glass;当你有自定义组件时,可以用以下几个 API 把同样的效果、动画与过渡搬到自己的视图上: | API | 作用 | 所属层级 | | --- | --- | --- | | `glassEffect(_:in:)` | 给单个视图加 Liquid Glass | 初级 | | `Glass.tint(_:)` / `Glass.interactive(_:)` | 配置色调 / 交互响应 | 初级 | | `buttonStyle(.glass)` | 标准按钮的玻璃样式 | 初级 | | `GlassEffectContainer(spacing:)` | 组合多个玻璃、优化渲染、支持融合与形变 | 中级 | | `glassEffectUnion(id:namespace:)` | 把多个静止形状合并为一个玻璃 | 中级 | | `glassEffectID(_:in:)` | 给玻璃分配身份,用于过渡形变 | 高级 | | `glassEffectTransition(_:)` | 选择 `matchedGeometry` 或 `materialize` 转场 | 高级 | 本教程分为初中高三个阶段,每个阶段都会给出**完整可编译、无 bug** 的代码,并讲解关键参数与设计取舍。 --- ## 初级篇:给单个视图加上 Liquid Glass 本阶段目标:学会用 `glassEffect` 给单个自定义视图加玻璃材质,并掌握形状、色调、交互三个配置维度。 ### 1.1 最基础的 glassEffect `glassEffect(_:in:)` 默认使用 `Glass.regular` 变体,并在视图内容背后以 `Capsule`(胶囊形)施加效果。 ```swift import SwiftUI struct BasicGlassView: View { var body: some View { // 在内容上加 .padding(),让玻璃有足够边距包裹文字 Text("Hello, World!") .font(.title) .padding() .glassEffect() // 默认:regular 玻璃 + Capsule 形状 } } #Preview { BasicGlassView() .frame(width: 320, height: 200) } ``` **讲解:** - `glassEffect()` 必须放在影响外观的修饰符(如 `.padding()`、`.font()`)**之后**。因为它会捕获内容交给容器渲染,放在前面会把 padding 等修饰漏在玻璃之外。 - 文字前景色会被系统自动调整为鲜艳且可读(vibrant),无需手动设置颜色。 ### 1.2 指定形状与圆角 胶囊形对大组件可能显得奇怪,此时可传入自定义 `Shape`,例如带圆角的矩形: ```swift import SwiftUI struct ShapedGlassView: View { var body: some View { VStack(spacing: 24) { Text("胶囊形(默认)") .font(.headline) .padding() .glassEffect() // Capsule Text("圆角矩形") .font(.headline) .padding() // 传入 in: 参数指定形状 .glassEffect(in: .rect(cornerRadius: 16.0)) Text("圆形") .font(.headline) .padding() .glassEffect(in: .circle) } } } #Preview { ShapedGlassView() .frame(width: 360, height: 360) } ``` **讲解:** - `glassEffect(in:)` 只改形状,玻璃材质仍是默认的 `regular`。 - 选择形状的原则:与同屏其他组件保持视觉一致。大块面板优先 `rect(cornerRadius:)`,图标/头像优先 `circle`。 ### 1.3 用 tint 表达语义 `tint` 给玻璃染色,用于**强调**或传达状态(例如警告用橙、成功用绿)。不要为了好看给所有玻璃都加色。 ```swift import SwiftUI struct TintedGlassView: View { var body: some View { HStack(spacing: 20) { Text("普通") .padding() .glassEffect() Text("强调") .padding() // 链式配置 Glass:.regular.tint(.orange) .glassEffect(.regular.tint(.orange)) } .font(.title3) } } #Preview { TintedGlassView() .frame(width: 420, height: 160) } ``` **讲解:** - `.regular.tint(.orange)` 是 `Glass` 结构的链式配置,返回一个新的 `Glass` 值再传给 `glassEffect(_:)`。 - tint 应承载含义(主操作、状态),而非纯装饰。 ### 1.4 用 interactive 让玻璃"活"起来 加 `.interactive()` 后,自定义组件会拥有和标准 `.glass` 按钮一样的响应式反应——按压时缩放、回弹、泛起涟漪般的光影。 ```swift import SwiftUI struct InteractiveGlassView: View { var body: some View { // 一个可点击的标签:组合 tint + interactive Label("一键收藏", systemImage: "star.fill") .font(.title3) .padding() .glassEffect(.regular.tint(.yellow).interactive()) } } #Preview { InteractiveGlassView() .frame(width: 320, height: 160) } ``` **讲解:** - `.interactive()` 让玻璃对触摸/指针做出流体反应,非常适合自定义控件或可交互的容器。 - 它和 `tint` 可以共存:`Glass.regular.tint(.yellow).interactive()`。 ### 1.5 用 .glass 按钮样式 如果你要的就是一个"玻璃按钮",不必自己用 `glassEffect` 拼,直接用系统 `buttonStyle(.glass)`: ```swift import SwiftUI struct GlassButtonView: View { var body: some View { VStack(spacing: 16) { Button("普通玻璃按钮") {} .buttonStyle(.glass) Button("强调玻璃按钮", systemImage: "bolt.fill") {} .buttonStyle(.glassProminent) // 想要显式形状,可用 buttonBorderShape Button("圆角矩形按钮") {} .buttonStyle(.glass) .buttonBorderShape(.roundedRectangle) } .font(.title3) } } #Preview { GlassButtonView() .frame(width: 320, height: 240) } ``` **讲解:** - `.glass` / `.glassProminent` 是系统提供的按钮样式,已经处理好玻璃材质、按压反馈、可读性,**优先使用它们**而不是手搓。 - 只有当标准按钮无法满足布局时,才回到 `glassEffect` 自定义。 ### 初级篇小结 ``` 单个视图加玻璃:glassEffect() 改形状:glassEffect(in: .rect(cornerRadius:)) 改材质:glassEffect(.regular.tint(.orange).interactive()) 按钮:buttonStyle(.glass) / .glassProminent ``` > 关键原则:**修饰顺序很重要**——`glassEffect` 要放在 `padding`/`font` 等外观修饰符之后。 --- ## 中级篇:用容器组合多个玻璃视图 本阶段目标:理解 `GlassEffectContainer` 的作用,掌握 `spacing` 对融合距离的影响,以及用 `glassEffectUnion` 合并静止形状。 ### 2.1 为什么需要 GlassEffectContainer 当你给多个视图分别加 `glassEffect` 时,如果它们各自独立渲染,会出现两个问题: 1. **性能下降**:每个玻璃都要单独采样背景,开销大。 2. **视觉不一致**:相邻的玻璃无法互相采样,折射效果可能不连贯。 `GlassEffectContainer` 把多个玻璃视图收进同一容器,统一采样背景、优化渲染,并让相邻玻璃的形状能够**融合**与**形变**。 ```swift import SwiftUI struct ContainerBasicView: View { var body: some View { // 一个容器包裹多个 glassEffect 视图 GlassEffectContainer(spacing: 40.0) { HStack(spacing: 40.0) { Image(systemName: "scribble.variable") .frame(width: 80, height: 80) .font(.system(size: 36)) .glassEffect() Image(systemName: "eraser.fill") .frame(width: 80, height: 80) .font(.system(size: 36)) .glassEffect() // 用 offset 演示玻璃之间如何互相反应 .offset(x: -40.0, y: 0.0) } } } } #Preview { ContainerBasicView() .frame(width: 400, height: 200) } ``` **讲解:** - 容器内每个带 `glassEffect` 的视图,其玻璃效果都会渲染在该视图背后。 - 上面用 `.offset(x: -40)` 让两个图标靠近,你可以观察到两块玻璃的形状开始融合——这正是容器的核心能力。 - **不要把相邻的玻璃分散到多个容器里**:不同容器无法互相采样,会产生不一致的折射。 ### 2.2 spacing 如何控制"融合"距离 `GlassEffectContainer(spacing:)` 的 `spacing` 决定**两块玻璃在多近的距离开始融合**: - `spacing` 越大:玻璃越早(在更远的距离)就开始融合,过渡更早发生。 - `spacing` 越小:需要靠得更近才会融合。 - **重要**:如果容器的 `spacing` **大于**内部 `HStack`/`VStack` 的 spacing,那么静止状态下玻璃就会因为靠得太近而**直接融合在一起**。 ```swift import SwiftUI struct SpacingDemoView: View { @State private var containerSpacing: Double = 40.0 var body: some View { VStack(spacing: 30) { // 容器 spacing 与 HStack spacing 相等,静止时各自独立 GlassEffectContainer(spacing: containerSpacing) { HStack(spacing: 40.0) { glassIcon("pencil.tip") glassIcon("scribble.variable") glassIcon("eraser.fill") } } // 滑块演示 spacing 的影响 Slider(value: $containerSpacing, in: 10...120) { Text("容器 spacing") } Text("当前 spacing = \(Int(containerSpacing))") .font(.caption) } .padding() } private func glassIcon(_ name: String) -> some View { Image(systemName: name) .frame(width: 64, height: 64) .font(.system(size: 28)) .glassEffect() } } #Preview { SpacingDemoView() .frame(width: 420, height: 280) } ``` **讲解:** - 拖动滑块增大 `spacing`,你会看到三个玻璃逐渐融合成一体;减小则重新分开。 - 当视图在容器内移动(动画进出)时,`spacing` 决定形变发生的时机——距离小于等于 spacing 时开始 morph。 ### 2.3 用 glassEffectUnion 合并静止形状 有时你希望多个视图的几何形状**即使在静止时也贡献给同一个玻璃形状**。用 `glassEffectUnion(id:namespace:)` 指定同一个 id,即可把形状相同、玻璃配置相同、id 相同的效果合并为一块玻璃。 ```swift import SwiftUI struct UnionGlassView: View { @Namespace private var namespace // 4 个符号:前 2 个合并为组 "1",后 2 个合并为组 "2" private let symbolSet: [String] = [ "cloud.bolt.rain.fill", "sun.rain.fill", "moon.stars.fill", "moon.fill" ] var body: some View { GlassEffectContainer(spacing: 20.0) { HStack(spacing: 20.0) { ForEach(symbolSet.indices, id: \.self) { item in Image(systemName: symbolSet[item]) .frame(width: 80, height: 80) .font(.system(size: 36)) .glassEffect() // 条件分组:前两个 -> "1",后两个 -> "2" .glassEffectUnion( id: item < 2 ? "1" : "2", namespace: namespace ) } } } } } #Preview { UnionGlassView() .frame(width: 420, height: 180) } ``` **讲解:** - `glassEffectUnion` 的 `id` 必须是 `Hashable & Sendable`,这里用字符串 `"1"`/`"2"`。 - 合并条件:**形状相同、Glass 配置相同、id 相同**。任一不同都不会合并。 - 适用场景:动态生成的视图,或位于 `HStack`/`VStack` 之外的视图,需要显式归组时。 ### 2.4 动态生成视图时的合并 当视图数量是动态的(例如来自数据),用 `glassEffectUnion` 按 id 归组可以让任意数量的视图合并到指定玻璃: ```swift import SwiftUI struct DynamicUnionView: View { @Namespace private var namespace struct Item: Identifiable { let id: String // 组 id,相同则合并 let symbol: String } private let items: [Item] = [ .init(id: "tools", symbol: "hammer"), .init(id: "tools", symbol: "wrench.adjustable"), .init(id: "tools", symbol: "screwdriver"), .init(id: "media", symbol: "play.circle"), .init(id: "media", symbol: "pause.circle") ] var body: some View { GlassEffectContainer(spacing: 24.0) { HStack(spacing: 24.0) { ForEach(items) { item in Image(systemName: item.symbol) .frame(width: 60, height: 60) .font(.system(size: 26)) .glassEffect() .glassEffectUnion(id: item.id, namespace: namespace) } } } } } #Preview { DynamicUnionView() .frame(width: 520, height: 140) } ``` **讲解:** - 用数据模型 `Item.id` 作为组 id,数据驱动分组,代码干净且可扩展。 - `ForEach` 用 `Identifiable` 模型,避免 `id: \.self` 在动态增删时引发动画错乱。 ### 中级篇小结 ``` 组合多玻璃:GlassEffectContainer(spacing:) { ... } 控制融合距离:容器 spacing vs 内部 HStack/VStack spacing 静止合并:glassEffectUnion(id:namespace:) 动态归组:用数据模型的 id 作为 union id ``` > 关键原则:相邻的玻璃要进**同一个**容器;union 的三要素(形状、Glass 配置、id)必须一致才会合并。 --- ## 高级篇:形变过渡、自定义转场与性能 本阶段目标:用 `glassEffectID` + 命名空间让玻璃在视图层级变化时**形变**(morph)而非简单淡入淡出,并掌握两种转场类型与性能要点。 ### 3.1 glassEffectID 与命名空间 形变发生在带玻璃效果的视图之间的**过渡/动画**期间。要用 `glassEffectID(_:in:)` 给每个玻璃分配一个**身份**,身份由 `@Namespace` 提供的命名空间管理。 - SwiftUI 用这些 id 判断"哪个形状要 morph 进哪个形状"。 - 当某个玻璃因视图层级变化而出现/消失时,系统结合容器 `spacing` 和形状几何,决定何时、把哪些形状 morph 进/出。 ```swift import SwiftUI struct MorphBasicView: View { @State private var isExpanded = false @Namespace private var namespace var body: some View { VStack(spacing: 20) { GlassEffectContainer(spacing: 40.0) { HStack(spacing: 40.0) { Image(systemName: "scribble.variable") .frame(width: 80, height: 80) .font(.system(size: 36)) .glassEffect() .glassEffectID("pencil", in: namespace) if isExpanded { Image(systemName: "eraser.fill") .frame(width: 80, height: 80) .font(.system(size: 36)) .glassEffect() .glassEffectID("eraser", in: namespace) } } } Button("切换") { withAnimation { isExpanded.toggle() } } .buttonStyle(.glass) } } } #Preview { MorphBasicView() .frame(width: 420, height: 260) } ``` **讲解:** - 容器 `spacing: 40`,`HStack` spacing 也是 `40`。当 `isExpanded` 切换时,橡皮擦图标出现/消失,它的最近边到铅笔图标的距离 ≤ 40,于是发生形变。 - `glassEffectID` 的第一个参数是**可选**的(`some Hashable & Sendable?`),只在过渡/动画期间影响内容。 ### 3.2 GlassEffectTransition:matchedGeometry 与 materialize `glassEffectTransition(_:)` 选择玻璃出现/消失时的转场类型: | 转场 | 行为 | 何时用 | | --- | --- | --- | | `.matchedGeometry`(默认) | 形状几何匹配地 morph 进/出相邻玻璃 | 两个玻璃距离 **≤ 容器 spacing** 时 | | `.materialize` | 更简单的"具象化"转场,自带动画 | 两个玻璃距离 **> 容器 spacing** 时 | ```swift import SwiftUI struct TransitionDemoView: View { @State private var showFar = false @Namespace private var namespace var body: some View { VStack(spacing: 24) { GlassEffectContainer(spacing: 20.0) { HStack(spacing: 20.0) { Image(systemName: "tray.fill") .frame(width: 64, height: 64) .font(.system(size: 28)) .glassEffect() .glassEffectID("tray", in: namespace) } } // 远距离出现的玻璃:用 materialize if showFar { Image(systemName: "paperplane.fill") .frame(width: 64, height: 64) .font(.system(size: 28)) .glassEffect() .glassEffectID("plane", in: namespace) // 距离远,用 materialize 而非 matchedGeometry .glassEffectTransition(.materialize) .padding(.top, 60) } Button(showFar ? "隐藏" : "显示") { withAnimation { showFar.toggle() } } .buttonStyle(.glass) } } } #Preview { TransitionDemoView() .frame(width: 320, height: 320) } ``` **讲解:** - 近距离(≤ spacing)默认 `matchedGeometry`,玻璃会顺着相邻形状"流"过去。 - 远距离用 `materialize`,避免 matchedGeometry 在远距离下显得突兀。 - 为了体验一致,整个 app 内同类交互应统一使用同一种转场。 ### 3.3 完整的展开/收起形变示例 下面是一个更完整的实战:一个工具按钮,点击后在它旁边展开一个功能盘,再次点击收起。玻璃随层级变化平滑形变。 ```swift import SwiftUI struct ExpandableToolDock: View { @State private var expanded = false @Namespace private var namespace private let tools: [(String, String)] = [ ("paintbrush", "画笔"), ("lasso", "套索"), ("scissors", "裁剪"), ("wand.and.rays", "魔棒") ] var body: some View { VStack(spacing: 24) { GlassEffectContainer(spacing: 32.0) { HStack(spacing: 32.0) { // 主按钮:始终存在,作为形变锚点 Image(systemName: "scribble.variable") .frame(width: 72, height: 72) .font(.system(size: 30)) .glassEffect(.regular.interactive()) .glassEffectID("anchor", in: namespace) // 展开时出现的工具盘 if expanded { HStack(spacing: 16.0) { ForEach(tools, id: \.0) { tool in Image(systemName: tool.0) .frame(width: 56, height: 56) .font(.system(size: 24)) .glassEffect(.regular.tint(.blue).interactive()) // 每个工具一个稳定 id,保证增删时形变正确 .glassEffectID(tool.0, in: namespace) } } .glassEffectID("toolGroup", in: namespace) } } } .animation(.snappy, value: expanded) Button(expanded ? "收起" : "展开") { expanded.toggle() } .buttonStyle(.glassProminent) } } } #Preview { ExpandableToolDock() .frame(width: 520, height: 220) } ``` **讲解:** - 主按钮用 `glassEffectID("anchor", ...)` 作为稳定锚点,工具盘整体用 `glassEffectID("toolGroup", ...)`,内部每个工具各有 id。 - `.animation(.snappy, value: expanded)` 让切换有弹性;`withAnimation` 也可。 - 用 `.interactive()` 让主按钮和工具都有按压反馈,手感更接近系统控件。 ### 3.4 性能优化要点 过多的容器和容器外的玻璃会拖慢渲染。要点如下: 1. **限制同屏玻璃数量**:不要给整个界面铺满 `glassEffect`。 2. **相邻玻璃放进同一个 `GlassEffectContainer`**:统一采样、统一渲染,既好看又省性能。 3. **标准控件优先**:能用 `buttonStyle(.glass)`、系统 toolbar、`NavigationSplitView` 自带玻璃的,就不要手搓。 4. **去掉与系统材质冲突的层**:自定义不透明背景、暗色遮罩、自绘 toolbar/sheet 填充会挡住 Liquid Glass,应移除。 5. **用 Instruments 排查**:参考 [Explore UI animation hitches and the render loop](https://developer.apple.com/videos/play/tech-talks/10855/) 与 [Optimize SwiftUI performance with Instruments](https://developer.apple.com/videos/play/wwc2025/306/) 定位卡顿。 ### 高级篇小结 ``` 形变身份:glassEffectID("id", in: namespace) + @Namespace 转场类型:.matchedGeometry(近,默认)/ .materialize(远) 触发形变:在 withAnimation { } 内改变视图层级 性能:同屏少而精、相邻同容器、优先用系统控件 ``` > 关键原则:`glassEffectID` 与 `glassEffectTransition` 只在**视图层级过渡/动画**期间生效,静止时不影响外观。 --- ## 引申应用场景案例 下面三个案例综合运用初中高全部知识,可直接作为项目模板。 ### 案例一:浮动媒体播放控件条 一个浮在视频/图片上方的播放控件条:左侧播放按钮、中间进度、右侧全屏。相邻控件共享容器,播放键用 tint 强调。 ```swift import SwiftUI struct FloatingMediaControls: View { @State private var isPlaying = false @State private var progress: Double = 0.35 var body: some View { GlassEffectContainer(spacing: 24.0) { HStack(spacing: 24.0) { // 播放/暂停:强调色 + 可交互 Button { isPlaying.toggle() } label: { Image(systemName: isPlaying ? "pause.fill" : "play.fill") .frame(width: 56, height: 56) .font(.system(size: 24)) } .glassEffect(.regular.tint(.blue).interactive()) // 进度条:圆角矩形玻璃 Slider(value: $progress, in: 0...1) .frame(width: 200) .padding(.horizontal, 8) .glassEffect(in: .rect(cornerRadius: 12)) // 全屏按钮:普通玻璃 Button { // 进入全屏 } label: { Image(systemName: "arrow.up.left.and.arrow.down.right") .frame(width: 56, height: 56) .font(.system(size: 22)) } .glassEffect(.regular.interactive()) } } } } #Preview { ZStack { // 模拟背景内容 LinearGradient(colors: [.indigo, .purple], startPoint: .topLeading, endPoint: .bottomTrailing) VStack { Spacer() FloatingMediaControls() .padding(.bottom, 24) } } .frame(width: 560, height: 320) } ``` **设计说明:** 播放键用 `tint(.blue)` 表达"主操作";进度条用 `rect(cornerRadius:)` 与按钮形状区分;全部放进一个容器保证渲染一致与性能。 ### 案例二:可展开的浮动工具盘 点击主按钮展开一排工具,收起时形变回收。综合 `glassEffectID` + `GlassEffectContainer` + `interactive`。 ```swift import SwiftUI struct FloatingToolDock: View { @State private var expanded = false @Namespace private var namespace private let tools: [String] = [ "format.bold", "format.italic", "format.underline", "textformat.abc.dottedunderline", "list.bullet" ] var body: some View { VStack { Spacer() GlassEffectContainer(spacing: 28.0) { HStack(spacing: 28.0) { Image(systemName: "textformat") .frame(width: 64, height: 64) .font(.system(size: 28)) .glassEffect(.regular.interactive()) .glassEffectID("dock", in: namespace) if expanded { ForEach(tools, id: \.self) { name in Image(systemName: name) .frame(width: 52, height: 52) .font(.system(size: 22)) .glassEffect(.regular.interactive()) .glassEffectID(name, in: namespace) } } } } .animation(.snappy, value: expanded) .padding(.bottom, 24) } .onTapGesture { expanded.toggle() } } } #Preview { FloatingToolDock() .frame(width: 600, height: 220) } ``` **设计说明:** 主锚点 `glassEffectID("dock", ...)` 始终在位;工具各自有稳定 id,增删时能正确形变。`onTapGesture` 触发展开/收起,`withAnimation` 改为隐式 `.animation(.snappy, value:)`。 ### 案例三:状态徽标集群 多个状态徽标按类别合并为两块玻璃(如"系统类"与"业务类"),用 `glassEffectUnion` 实现。 ```swift import SwiftUI struct StatusBadgeCluster: View { @Namespace private var namespace struct Badge: Identifiable { let id: String // 类别 id,用于 union let symbol: String let title: String } private let badges: [Badge] = [ .init(id: "system", symbol: "wifi", title: "网络"), .init(id: "system", symbol: "battery.100", title: "电量"), .init(id: "business", symbol: "checkmark.seal", title: "已认证"), .init(id: "business", symbol: "shield.lefthalf.filled", title: "安全") ] var body: some View { GlassEffectContainer(spacing: 20.0) { HStack(spacing: 20.0) { ForEach(badges) { badge in Label(badge.title, systemImage: badge.symbol) .font(.caption) .padding(.horizontal, 12) .padding(.vertical, 8) .glassEffect() .glassEffectUnion(id: badge.id, namespace: namespace) } } } } } #Preview { StatusBadgeCluster() .frame(width: 520, height: 80) } ``` **设计说明:** 系统类徽标合并为一块玻璃,业务类合并为另一块,视觉上自然分组;数据驱动 id,新增类别只需改数据。 --- ## 附录:API 速查表 | API | 签名概要 | 用途 | | --- | --- | --- | | `glassEffect(_:in:)` | `func glassEffect(Glass = .regular, in: some Shape = Capsule) -> someView` | 给视图加 Liquid Glass | | `Glass.tint(_:)` | `func tint(Color) -> Glass` | 给玻璃染色 | | `Glass.interactive(_:)` | `func interactive(Bool = true) -> Glass` | 启用触摸/指针响应 | | `GlassEffectContainer` | `init(spacing: Double, content:)` | 组合多玻璃、优化渲染、支持融合与形变 | | `glassEffectUnion(id:namespace:)` | `func glassEffectUnion(id: (some Hashable & Sendable)?, namespace: Namespace.ID) -> someView` | 合并静止形状为一块玻璃 | | `glassEffectID(_:in:)` | `func glassEffectID((some Hashable & Sendable)?, in: Namespace.ID) -> someView` | 给玻璃分配身份,用于过渡形变 | | `glassEffectTransition(_:)` | `func glassEffectTransition(GlassEffectTransition) -> someView` | 选择 `.matchedGeometry` / `.materialize` 转场 | | `buttonStyle(.glass)` | `GlassButtonStyle` | 标准玻璃按钮样式 | | `buttonStyle(.glassProminent)` | `GlassProminentButtonStyle` | 强调玻璃按钮样式 | ### 黄金法则回顾 1. **系统控件优先**:能用 `buttonStyle(.glass)`、toolbar、`NavigationSplitView` 自带玻璃的,就不要手搓。 2. **修饰顺序**:`glassEffect` 放在 `padding`/`font` 等外观修饰符**之后**。 3. **相邻同容器**:相邻的玻璃必须进同一个 `GlassEffectContainer`。 4. **tint 有语义**:颜色传达含义,不为装饰而染色。 5. **身份要稳定**:形变用 `glassEffectID` + `@Namespace`,动态列表用 `Identifiable` 模型。 6. **同屏少而精**:限制玻璃数量,用 Instruments 排查性能。 --- > 参考资源: > - [Applying Liquid Glass to custom views](https://developer.apple.com/documentation/swiftui/applying-liquid-glass-to-custom-views) > - [Landmarks: Building an app with Liquid Glass](https://developer.apple.com/documentation/swiftui/landmarks-building-an-app-with-liquid-glass) > - [Explore UI animation hitches and the render loop](https://developer.apple.com/videos/play/tech-talks/10855/) > - [Optimize SwiftUI performance with Instruments](https://developer.apple.com/videos/play/wwc2025/306/)