Dive deep into volumes and immersive spaces
System Frameworks 进阶 20m

深入理解 visionOS 的 Volume 和沉浸空间

Dive deep into volumes and immersive spaces

2024年6月10日

在 Apple 官方观看视频

一句话判断

visionOS 2 为 Volume 带来了底板(Baseplate)、可调整大小、工具栏和装饰件(Ornament)四大新能力,让体积窗口从”固定容器”变成”可交互的弹性空间”。

这场 Session 讲了什么

visionOS 有三种场景类型:Window、Volume 和 Immersive Space。这场 Session 聚焦在 Volume 和 Immersive Space 上,尤其是 visionOS 2 对 Volume 的增强。

当一个应用链接到 visionOS 2 SDK 后,Volume 会自动出现底板(Baseplate)。底板在你注视它时淡入显示,帮助感知 Volume 的边界范围。如果你的内容已经填满了 Volume 边界或者自己绘制了底部表面,可以关闭底板避免冲突。

visionOS 2 的 Volume 新增了角落的调整大小手柄,跟 Window 一样。Volume 的最小/最大尺寸默认从内容视图的 frame 继承。如果你给视图设置了固定尺寸,Volume 就不能调整大小。改为设置最小/最大值后,拖动手柄就能平滑缩放。你也可以通过代码动态改变状态变量来驱动 Volume 尺寸变化。

工具栏(Toolbar)可以浮动在 Volume 底部的装饰件中。visionOS 2 的工具栏会自动跟随用户站立的方位移动到最近的侧面,窗口控件也一样。除了工具栏,visionOS 2 还允许 Volume 添加自定义装饰件,可以放在 Volume 周围的任意位置,并且会根据距离自动缩放保持可读性。

值得深挖的点

Volume 尺寸机制:内容驱动 vs 手动控制

Volume 的尺寸行为默认是 contentSize 模式——Volume 的最小和最大尺寸都从其内容的 frame 推导。这在实际开发中有一套优雅的交互方式。

给视图设置 .frame(width: 400, height: 300, depth: 300) 固定值,Volume 就锁定在这个尺寸,用户拖动手柄会弹回。改为 .frame(minWidth: 300, minHeight: 200, minDepth: 200) 后,Volume 允许用户拖大,但不会小于这个最小值。

更妙的是你可以用 @State 变量驱动尺寸变化。比如一个切换按钮在”小场景”和”大场景”之间切换,@State 变量改变后 frame 跟着变,Volume 自动调整边界。这种方式下内容不会被裁切,因为 Volume 总是匹配内容尺寸。调整大小的手柄也始终在内容附近,用户不需要去够很远的地方找控件。

装饰件的正确使用姿势

装饰件(Ornament)是 visionOS 2 给 Volume 新增的重要能力。它可以把辅助信息和控件从主视图中剥离出来,浮动在 Volume 周围。

工具栏是系统提供的装饰件,适合放常用操作按钮。自定义装饰件则适合放进度条、状态信息、辅助详情等内容。装饰件会自动根据 Volume 的距离缩放——当你把 Volume 推远时,装饰件仍然保持可读的尺寸。

但 Session 里有个明确警告:不要过度使用装饰件。太多装饰件会挤占内容的空间,而内容才是应用的主角。建议用一个装饰件来包含一组相关的控件和信息。同时要注意避开系统提供的工具栏和 TabView 的位置,避免冲突。

代码片段

控制底板显示和 Volume 尺寸

// visionOS 2 底板默认开启,注视时自动显示
// 如果内容已填满边界,可以关闭底板
VolumeViewer()
    .volumeBaseplateVisibility(.hidden)

// 通过状态变量驱动 Volume 尺寸变化
struct ContentView: View {
    @State private var isExpanded = false
    
    var body: some View {
        ExplorationView()
            .frame(
                width: isExpanded ? 600 : 400,
                height: isExpanded ? 500 : 300,
                depth: isExpanded ? 500 : 300
            )
            // 按钮切换大小
            .overlay {
                Button(isExpanded ? "缩小" : "放大") {
                    withAnimation {
                        isExpanded.toggle()
                    }
                }
            }
    }
}

坑点:如果忘记设置 frame 的最小值而只设了固定值,用户拖动调整手柄时会弹回原尺寸,体验很奇怪。确保用 minWidth/minHeight/minDepth 而不是固定 width/height/depth

为 Volume 添加工具栏

// 工具栏自动浮动在 Volume 底部
ContentView()
    .toolbar {
        // visionOS 2 中 .bottomOrnament 是默认放置位置
        // 但为了兼容 visionOS 1 需要显式指定
        ToolbarItemGroup(placement: .bottomOrnament) {
            Button("种植") { plant() }
            Button("浇水") { water() }
            Button("收获") { harvest() }
        }
    }

自定义装饰件

// 将进度视图从 Volume 主体移到装饰件中
// 这样它会根据距离自动缩放,且跟随视角更新
ContentView()
    .ornament(
        visibility: .visible,
        attachmentAnchor: .scene(.bottom)
    ) {
        // 种植进度面板
        PlantingProgressView(
            planted: plantedCount,
            goal: goalCount
        )
        .padding()
        .glassBackgroundEffect()
    }

坑点:装饰件的 attachmentAnchor 决定它相对于 Volume 的位置。使用 .scene(.bottom) 时注意不要与工具栏重叠。建议自定义装饰件放在顶部或侧面,底部留给工具栏。

最佳实践

从 visionOS 1 迁移到 visionOS 2 时,首先确认底板行为是否符合预期——如果内容已有自己的底部视觉,加上 .volumeBaseplateVisibility(.hidden)。然后审查 Volume 内容的 frame 设置,确保使用最小/最大值而非固定值来启用调整大小。最后把散落在主视图中的辅助 UI 提取到装饰件中,主视图只保留核心 3D 内容。

还有什么值得关注

  • Volume 的工具栏和窗口控件在 visionOS 2 会自动跟随用户方位移到最近的侧面
  • Immersive Space 部分在 Session 后半段有详细演示,适合需要全房间沉浸体验的应用
  • 装饰件的 .glassBackgroundEffect() 可以给控件加上毛玻璃背景,与系统风格一致
WWDC 2024