← 所有文章

超越窗口的 visionOS 空间模式

如今登陆 visionOS 的 App,大多是走 Apple 的“Designed for iPad”兼容路径上来的:现成的 iPad 二进制文件在三维空间里以一块扁平面板的形式浮着,开发者勾一个选项框,而不是真去做一套 visionOS 原生体验。这条路对用户没什么不好(App 能用),但这等于贱卖了平台的价值。visionOS 的原生界面层给开发者准备了三种呈现方式——窗口(Window)、立体窗口(Volume)、沉浸式空间(Immersive Space)——外加 iPad SDK 所没有的结构性 UI 原语:装饰件(ornament)与附件(attachment)。4 用上它们的 App 才有原生感;不用的,读起来就是“跑在 Vision 上的 iPad App”。

本文对照 Apple 官方文档,把这套空间词汇逐个走一遍。切入角度不是“visionOS 入门”,而是“平台到底给一个 SwiftUI App 提供了什么”。本系列的 RealityKit 与空间心智模型 讲的是 3D 内容层;这一篇讲的是承载它的 SwiftUI 界面层。

TL;DR

  • visionOS App 由三类场景组合而成:WindowGroup(窗口)、带 .windowStyle(.volumetric)WindowGroup(立体窗口),以及 ImmersiveSpace(沉浸式空间)1
  • 窗口是一块二维平面;立体窗口是一段有边界的三维区域;沉浸式空间则把用户整个包住。三者规则各异:立体窗口创建之后尺寸不可变,沉浸式空间必须显式打开和关闭,窗口的行为最接近 iPad。
  • 沉浸分三种风格:.mixed(虚拟内容与现实房间共存)、.full(房间被虚拟环境替换)、.progressive(介于两者之间,余光里保留现实参照)2
  • 装饰件是与窗口平行、在 z 轴上略微靠前的 UI 平面,visionOS 的工具栏和标签栏就是这么做的3。附件则把 SwiftUI 视图嵌进 RealityView 的 3D 内容里,是扁平 UI 与空间几何之间的桥。
  • “面板式 App”反模式:把 iPad 界面原样当成一个窗口发出去,不用立体窗口、不用沉浸式空间、不用装饰件。用户照样能用,但平台真正的价值一分未取。

三种场景类型

visionOS App 的 App body 由三类场景组合而成,每一类对应一种截然不同的用户心智模型。

窗口:二维平面

WindowGroup 默认产出一个带 visionOS 玻璃边框的二维窗口。窗口被放置在空间中(系统把它摆在用户视线正前方),用户可以通过系统标准手势移动它、调整它的大小。从 SwiftUI 的角度看,窗口就是 visionOS 版的 macOS 窗口:一块扁平的内容面,外面裹着一层带景深感的玻璃材质。

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

默认窗口会在内容外围包一层玻璃材质。若希望承载面完全透明,使用 .windowStyle(.plain)

WindowGroup {
    ContentView()
}
.windowStyle(.plain)

plain 样式的窗口会失去系统玻璃边框。只有当内容自带视觉容器时才用它;其余情况,默认值就是对的。

立体窗口:有边界的三维区域

立体窗口是一段容纳景深内容的三维区域——一个模型、一个包含多个物体的场景,或者一套确实能从第三根轴上获益的 UI。立体场景同样是 WindowGroup,只是样式不同:

WindowGroup(id: "globe") {
    GlobeView()
}
.windowStyle(.volumetric)
.defaultSize(width: 0.6, height: 0.6, depth: 0.6, in: .meters)

.defaultSize(width:height:depth:in:) 修饰符用现实世界的单位(米)指定立体窗口的边界。默认情况下,边界在打开的那一刻就固定下来,用户可以移动它,却不能调整它的大小。visionOS 2 及以后通过 .windowResizability(.contentSize) 及相关 API 提供了一条需要主动开启的路径,供确实需要让用户调整立体窗口大小的 App 使用;固定尺寸的默认行为仍是绝大多数情况。由此得出一条:默认尺寸要慎重挑选,因为除非开发者显式开启,大多数立体窗口都是不能调整大小的。

真正适合立体窗口的,是那些“空间边界本身就是体验一部分”的 App:一件用户可以绕着走的虚拟雕塑、一把钉在真实墙面上的卷尺、一个目标物按深度错落排布的健身场景。只是想要更大画布的 App 从立体窗口里得不到什么好处——把窗口做大才是正解。

沉浸式空间:环绕

ImmersiveSpace 是一种占据用户四周环境的场景。5 窗口和立体窗口都停留在共享空间里,与其他 App 并排可见;沉浸式空间则不同,它接管用户的整个周遭,此时也就无法再同时使用其他 App 的窗口。

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }

        ImmersiveSpace(id: "training") {
            TrainingScene()
        }
        .immersionStyle(selection: .constant(.mixed), in: .mixed, .progressive, .full)
    }
}

.immersionStyle(...) 修饰符决定体验的层级:

  • .mixed 虚拟内容与真实房间并存。适用于用户同时需要两种情境的 App。
  • .progressive 部分沉浸,用数码表冠(Digital Crown)无级调节深浅。中央视野是虚拟的,余光里仍能感知房间。
  • .full 房间被虚拟环境完全替换。用于彻底沉浸的体验:冥想、训练模拟、游戏。

打开沉浸式空间这件事必须显式发生。App 通过 @Environment(\.openImmersiveSpace) 传入空间的 id;过渡动画,以及与之冲突的空间的关闭,都由系统负责:

@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace

Button("Start Session") {
    Task {
        await openImmersiveSpace(id: "training")
    }
}

每个 App 同一时刻只能有一个沉浸式空间处于活动状态。空间之间的切换(例如从 .mixed 换到 .full)需要先显式关闭旧的,再打开新的。

装饰件:环绕窗口的 UI 平面

装饰件是附着在窗口边缘的 SwiftUI 视图,在 z 轴上比窗口平面略微靠前。visionOS 的工具栏、标签栏和辅助控件都是这么实现的。系统自身也处处在用:“TV”的播放控件、“音乐”里的分段控件、“邮件”的工具栏,都是装饰件。

ContentView()
    .ornament(
        attachmentAnchor: .scene(.bottom),
        contentAlignment: .center
    ) {
        HStack {
            Button("Previous", systemImage: "backward.fill") { ... }
            Button("Play", systemImage: "play.fill") { ... }
            Button("Next", systemImage: "forward.fill") { ... }
        }
        .padding()
        .glassBackgroundEffect()
    }

attachmentAnchor: 参数指定装饰件相对窗口的位置:.scene(.top).scene(.bottom).scene(.leading).scene(.trailing)。装饰件的视觉处理由开发者自己负责;.glassBackgroundEffect() 会生成与窗口边框相称的 visionOS 原生玻璃材质。

装饰件解决的是 visionOS 上一个实打实的问题:控件塞进窗口里会挤压内容,放到另一个窗口里又逼着用户重新对准视线。装饰件悬在用户的余光范围内,随时可以用注视选中,却不会跟主内容争夺中央视野。

RealityView 附件:把 SwiftUI 放进三维空间

当 App 需要在三维场景内部放置 SwiftUI 视图时——3D 模型上的一个标签、悬在虚拟物体旁的一个按钮、钉在现实表面上的一段测量读数——桥梁就是 RealityView 的附件机制。

RealityView { content, attachments in
    let model = ModelEntity(...)
    content.add(model)

    if let label = attachments.entity(for: "label") {
        label.position = [0, 0.5, 0]
        model.addChild(label)
    }
} attachments: {
    Attachment(id: "label") {
        Text("Vintage Globe, 1872")
            .padding()
            .glassBackgroundEffect()
    }
}

attachments: 闭包声明带稳定标识符的 SwiftUI 视图。在主 RealityView 闭包里,attachments.entity(for:) 把视图取回成一个 3D Entity,可以在场景坐标系中定位。这个视图照常参与 SwiftUI 的更新周期(状态一变就重绘),同时在三维场景中以带纹理的平面呈现。

凡是需要嵌在世界之中的 UI,这套机制都是正解:跟着运动物体走的标签、测量标注、随情境出现的按钮。SwiftUI 视图的写法一如往常,三维定位则发生在 RealityView 这一层。

“面板式 App”反模式

visionOS 上最常见的发布失误就是面板式 App:一个 iPad App 靠“Designed for iPad”兼容登陆 visionOS,以单一窗口的形态发布,没有立体窗口,没有沉浸式空间,也没有装饰件。App 能用,但它并没有真正配得上这个平台。

判断一个 App 是不是面板式,有三个信号:

只有一个窗口场景。 没有 .windowStyle(.volumetric),没有声明 ImmersiveSpace。整个 App 就是一块扁平的面,仅此而已。

没有采用装饰件。 标签栏长在窗口内容里,而不是内容之外。同样的内容密度下,它看上去就比 visionOS 原生 App 拥挤。

没有任何只有空间里才成立的功能。 App 完全没用到第三根轴:立体窗口里没有 3D 模型,空间里没有环境场景,也没有借助附件做 z 轴上的 UI 布局。它在 iPad 上做什么,现在还做什么,只是浮起来了。

面板式 App 本身并不是失败。对那些从空间计算里得不到什么的内容品类——聊天 App、笔记 App、设置类小工具——它就是正确选择。真正的失败模式是:发了一个面板式 App,却还要以 visionOS 原生自居。本系列的 Apple 平台矩阵 一文主张,要不要上某个平台是产品决策;对 visionOS 而言,这个决策是:“这个 App 该不该去挣那块空间界面,还是一块面板就够了?”

常见的失败

三种会造出糟糕 visionOS UX 的模式:

名为立体窗口,实为加了深度留白的二维内容。 一套占满立体窗口、内部却只渲染扁平面的“3D”UI,只是在白白浪费空间。立体窗口是给三维内容准备的;扁平内容属于窗口。

沉浸风格与使用场景相抵触。 一个只提供 .full 沉浸的冥想 App,会为了几分钟的短练习把用户硬拽出所处环境。一个只提供 .mixed 的训练 App,在需要高度专注的练习里又走得不够远。沉浸风格要与用户真实的使用场景相匹配。

装饰件与内容抢戏。 装饰件生来就属于余光。一个非要占据中央注意力的装饰件——闪烁的颜色、动来动去的动画——恰恰适得其反。装饰件应当承载稳定的、扫一眼就能看明白的控件。

这套模式对 visionOS App 意味着什么

三条结论。

  1. 按用户的心智模型挑场景类型,而不是按哪种省事。 一列扁平的条目是窗口。一个供用户端详的 3D 模型是立体窗口。一片环绕四周的环境是沉浸式空间。把它们混编进一个 App 里——一个窗口,按需打开一个立体窗口;沉浸式空间从窗口上的按钮进入——才是 visionOS 的原生做法。

  2. 工具栏和辅助 UI 请用装饰件。 visionOS 就是靠装饰件传达“这部分 UI 是附属的”;把工具栏塞进窗口内容里,读起来就是跑在 Vision 上的 iPad。接入成本很小,视觉差别很大。

  3. RealityView 里的世界内 UI 请用附件。 3D 物体上的标签、虚拟内容旁的按钮、随情境出现的读数。SwiftUI 与三维空间之间的桥已经修好了;失败模式是不去用它,最后拿零散的 3D 文字渲染凑合。

完整的 Apple 生态系列:带类型的 App IntentsMCP 服务器路由该怎么选Foundation Models执行环境与工具链的 LLM 之分三个界面单一事实来源模式两个 MCP 服务器面向 Apple 开发的钩子Live ActivitieswatchOS 执行环境SwiftUI 的内部构造RealityKit 的空间心智模型SwiftData 的 schema 纪律Liquid Glass 模式多平台发布平台矩阵Vision 框架符号动效词汇Core ML 端侧推理Writing Tools APISwift Testing隐私清单深入解析把无障碍当作平台能力SF Pro 字体系统我拒绝写的东西。系列主页在 Apple 生态系列。想了解更宽的“iOS 与 AI 代理”背景,请参阅 iOS 代理开发指南

常见问题

立体窗口和沉浸式空间有什么区别?

立体窗口是一段有边界的三维区域,与其他 App 一起待在共享空间里。用户可以绕着它走,系统会给它加上边框,其他 App 的窗口也依然可见。沉浸式空间则把用户包围起来,接管整个环境,其他 App 无法同时使用。立体窗口对应“看看这个三维的东西”;沉浸式空间对应“待在这个环境里”。

可以同时打开多个立体窗口吗?

可以。多个带 .volumetricWindowGroup 场景能够同时打开,各有各的尺寸和内容,系统会在空间中分别为它们定位。

可以同时打开多个沉浸式空间吗?

不行。每个 App 同一时刻只能有一个沉浸式空间处于活动状态。空间之间的切换,必须通过 @Environment(\.openImmersiveSpace)@Environment(\.dismissImmersiveSpace) 先显式关闭当前的,再打开新的。

立体窗口的尺寸真的不可变吗?

默认情况下,立体窗口的边界在打开时就固定了。visionOS 人机界面指南(HIG)的说法是:立体窗口代表的是带有明确边界意图的特定三维内容,任由用户随意调整大小会扭曲内容原本的比例。visionOS 2 及以后新增了由开发者主动开启的尺寸可调立体窗口,途径是 .windowResizability(.contentSize) 及相关 API,确实需要让用户调整空间容器大小的 App 可以主动启用。绝大多数立体窗口仍以固定尺寸的默认值发布——对有明确物理比例的内容(虚拟雕塑、按真实尺寸建模的物件),HIG 至今推荐的也正是这个默认值。

怎样给 visionOS 窗口加标签栏?

要做内容之内的标签(iPad 式做法),在窗口里用 TabView;要做 visionOS 原生的外围标签 UI,就用装饰件配自定义按钮行。装饰件这条路正是 Apple 自家 App(音乐、邮件)在走的,对 visionOS 用户来说也最有原生感。

RealityView 附件能与手部追踪交互吗?

可以。附件一经定位就是三维实体,与其他 RealityKit 实体共用同一套手势与命中测试系统。点按、拖拽、悬停手势都通过 SwiftUI 标准的手势修饰符挂上去;本系列的 RealityKit 一文 讲了手部追踪的集成模式。

参考资料


  1. Apple Developer:Meet SwiftUI for spatial computing(WWDC 2023 场次 10109)。介绍 WindowGroup、volumetric 版 WindowGroup 与 ImmersiveSpace 这三种 visionOS 场景类型。 

  2. Apple 开发者文档:ImmersionStyle。三种沉浸风格(.mixed.progressive.full)以及 .immersionStyle(selection:in:) 修饰符的 API。 

  3. Apple 开发者文档:ornament(visibility:attachmentAnchor:contentAlignment:ornament:)。按指定锚点为窗口添加装饰件 UI 平面的 SwiftUI 视图修饰符。 

  4. Apple Developer:Go beyond the window with SwiftUI(WWDC 2023 场次 10111)。这一场讲的是立体窗口、沉浸式空间,以及在 visionOS 上超越扁平面板 UI 的种种模式。 

  5. Apple 开发者文档:Creating an immersive space in visionOS with SwiftUI。从定义到打开沉浸式空间的完整指南。 

相关文章

RealityKit与空间心智模型

RealityKit是一个实体-组件-系统架构,而非3D版的SwiftUI。锚点将实体置于真实空间中。该模型与窗口的五大区别。

11 分钟阅读

2026 年的 RealityKit 与 Reality Composer Pro 3

WWDC26 让 Apple 的空间流程趋于成熟:RealityKit 新增光照与布料模拟,并推出具备实时预览与 Xcode 插件的独立版 Reality Composer Pro 3。

13 分钟阅读

安装与更新 Codex CLI:Mac、Linux、Windows

安装、更新、锁定版本与卸载 OpenAI Codex CLI 的每一种方式:安装脚本、npm、Homebrew、winget,覆盖 macOS、Linux、WSL 与 Windows。

9 分钟阅读