Highlight
SwiftUI 在 visionOS 上引入了 ImmersiveSpace 场景类型,与 WindowGroup 和 Volume 并列,让应用内容突破窗口边界,通过 openImmersiveSpace/dismissImmersiveSpace 环境动作控制进入和退出,配合 RealityView 和 ARKit 实现从混合现实到完全沉浸的多种体验层级。
核心内容
三种容器,三种体验层级
visionOS 上 SwiftUI 提供了三种场景容器:WindowGroup 用于传统窗口,Volume 用于有边界的 3D 内容,ImmersiveSpace 用于无边界的空间体验。前两者将内容限制在容器内,而 ImmersiveSpace 让你的内容可以出现在用户周围的任何地方。
当应用打开 ImmersiveSpace 时,进入 Full Space 模式。其他应用全部隐藏,为你的内容让出整个空间。关闭后,其他应用自动恢复。应用同时只能打开一个 Space。
(03:00)
定义和打开 ImmersiveSpace
定义一个 ImmersiveSpace 只需要几行代码:
@main
struct WorldApp: App {
var body: some Scene {
ImmersiveSpace(id: "solar") {
SolarSystem()
}
}
}
关键点:
ImmersiveSpace是新的场景类型,与WindowGroup用法类似- 通过
id参数为 Space 命名,后续通过该标识符打开 - Space 的原点位于用户脚下,内容放置需要相对这个位置考虑
从窗口中打开 Space 需要使用环境动作:
struct SpaceControl: View {
@Environment(\.openImmersiveSpace) private var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
@State private var isSpaceHidden: Bool = true
var body: some View {
Button(isSpaceHidden ? "View Outer Space" : "Exit the solar system") {
Task {
if isSpaceHidden {
let result = await openImmersiveSpace(id: "solar")
switch result {
case .opened:
isSpaceHidden = false
case .error:
// 处理打开失败
break
default:
break
}
} else {
await dismissImmersiveSpace()
isSpaceHidden = true
}
}
}
}
}
关键点:
openImmersiveSpace和dismissImmersiveSpace是环境值,通过@Environment获取openImmersiveSpace(id:)返回异步结果,需要处理成功和失败两种情况dismissImmersiveSpace()不需要参数,因为同时只能有一个 Space 打开- 这些动作是 async 的,系统会在 Space 进入/退出动画完成后返回
(09:11)
RealityView 与 ImmersiveSpace 的配合
ImmersiveSpace 和 RealityView 是配套设计的。RealityView 提供异步加载、ARKit 锚点放置、手部和头部姿态数据访问等能力:
ImmersiveSpace {
RealityView { content in
let starfield = await loadStarfield()
content.add(starfield)
}
}
关键点:
- RealityView 的闭包支持 async,可以异步加载 3D 资源
- 加载完成后通过
content.add()将实体添加到场景中 - RealityView 使用 RealityKit 渲染,坐标系与 SwiftUI 不同
坐标系差异需要注意:
- SwiftUI 中 y 轴向下,z 轴朝向用户
- RealityKit 中 y 轴向上
- 在 RealityView 中定位实体时遵循 RealityKit 的坐标约定
(06:53)
详细内容
多场景应用结构
一个典型的 visionOS 应用同时包含窗口和 Space:
@main
struct WorldApp: App {
var body: some Scene {
// 第一个场景是启动时显示的窗口
WindowGroup {
VStack {
Text("The Solar System")
.font(.largeTitle)
SpaceControl()
}
}
// ImmersiveSpace 默认不显示,需要手动打开
ImmersiveSpace(id: "solar") {
SolarSystem()
}
}
}
关键点:
- 场景列表中第一个场景在应用启动时自动显示
- ImmersiveSpace 默认隐藏,通过按钮等交互触发打开
- 也可以配置应用直接启动进入 Space(见下文)
(10:44)
异步加载 3D 模型
3D 资源加载需要时间,Model3D 提供了加载状态管理:
Model3D(named: "Earth") { phase in
switch phase {
case .empty:
Text("Waiting")
case .failure(let error):
Text("Error \(error.localizedDescription)")
case .success(let model):
model.resizable()
}
}
关键点:
phase参数反映加载状态:empty、failure、success- 在 empty 状态显示加载指示,failure 显示错误信息
- success 时获得加载完成的模型,可应用
.resizable()等修饰符
(11:32)
Scene Phase 管理
ImmersiveSpace 支持 SwiftUI 的 scene phase,可以响应活跃状态变化:
@main
struct WorldApp: App {
@EnvironmentObject private var model: ViewModel
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
ImmersiveSpace(id: "solar") {
SolarSystem()
.onChange(of: scenePhase) {
switch scenePhase {
case .inactive, .background:
model.solarEarth.scale = 0.5
case .active:
model.solarEarth.scale = 1.0
default:
break
}
}
}
}
}
关键点:
- Space 会在用户走出边界或系统警报弹出时进入 inactive 状态
- 用户回到体验区域后恢复 active 状态
- 利用 scene phase 变化可以给出视觉反馈,如缩小内容提示状态变化
(13:04)
坐标转换
窗口和 Space 各自有独立的坐标系。SwiftUI 提供了 ImmersiveSpace 坐标空间用于转换:
GeometryReader3D { proxy in
Earth()
.onTapGesture {
if let translation = proxy.transform(in: .immersiveSpace)?.translation {
model.solarEarth.position = Point3D(translation)
}
}
}
关键点:
GeometryReader3D提供 3D 几何信息proxy.transform(in: .immersiveSpace)获取当前视图在 ImmersiveSpace 坐标系中的变换- 通过转换可以将窗口中的点击位置映射到 Space 中的实体位置
(14:21)
三种 Immersion Style
visionOS 提供三种沉浸样式,控制内容如何接管周围环境:
@main
struct WorldApp: App {
@State private var currentStyle: ImmersionStyle = .mixed
var body: some Scene {
ImmersiveSpace(id: "solar") {
SolarSystem()
.simultaneousGesture(
MagnifyGesture()
.onChanged { value in
let scale = value.magnification
if scale > 10 {
currentStyle = .full
} else if scale > 5 {
currentStyle = .progressive
} else {
currentStyle = .mixed
}
}
)
}
.immersionStyle(selection: $currentStyle, in: .mixed, .progressive, .full)
}
}
关键点:
.mixed(默认):内容与现实环境共存,可以看到周围的一切.progressive:内容在一个前方 portal 中显示,可通过 Digital Crown 调节沉浸度.full:完全接管视野,用户完全沉浸在虚拟内容中.immersionStyle(selection:in:)修饰符声明支持的样式列表- 用户可以通过 Digital Crown 在 progressive 样式中调节 passthrough 比例
(16:34)
环境效果与手部隐藏
在 progressive 和 full 样式中,可以进一步调整体验:
ImmersiveSpace(id: "solar") {
SolarSystem()
.preferredSurroundingsEffect(.systemDark)
}
.immersionStyle(selection: $currentStyle, in: .progressive)
关键点:
.preferredSurroundingsEffect(.systemDark)让周围环境变暗,突出 Space 内容- 仅在 progressive 和 full 样式中有效
在 full 样式中隐藏真实手部,显示虚拟手部:
ImmersiveSpace(id: "solar") {
SolarSystem()
}
.immersionStyle(selection: $currentStyle, in: .full)
.upperLimbVisibility(.hidden)
关键点:
.upperLimbVisibility(.hidden)在 full immersion 下隐藏真实手部- 配合 ARKit 手部追踪可以显示虚拟手部模型
(20:08)
ARKit 手部追踪与虚拟手套
通过 ARKit 获取手部骨骼数据,驱动虚拟手套模型:
struct SpaceGloves: View {
let arSession = ARKitSession()
let handTracking = HandTrackingProvider()
var body: some View {
RealityView { content in
let root = Entity()
content.add(root)
let leftGlove = try! Entity.loadModel(named: "assets/gloves/LeftGlove_v001.usdz")
root.addChild(leftGlove)
let rightGlove = try! Entity.loadModel(named: "assets/gloves/RightGlove_v001.usdz")
root.addChild(rightGlove)
Task {
do {
try await arSession.run([handTracking])
} catch let error as ProviderError {
print("Provider error: \(error.localizedDescription)")
} catch {
print("Unexpected error: \(error.localizedDescription)")
}
for await anchorUpdate in handTracking.anchorUpdates {
let anchor = anchorUpdate.anchor
switch anchor.chirality {
case .left:
leftGlove.transform = Transform(matrix: anchor.transform)
for (index, jointName) in anchor.skeleton.definition.jointNames.enumerated() {
leftGlove.jointTransforms[index].rotation = simd_quatf(
anchor.skeleton.joint(named: jointName).localTransform
)
}
case .right:
rightGlove.transform = Transform(matrix: anchor.transform)
for (index, jointName) in anchor.skeleton.definition.jointNames.enumerated() {
rightGlove.jointTransforms[index].rotation = simd_quatf(
anchor.skeleton.joint(named: jointName).localTransform
)
}
}
}
}
}
}
}
关键点:
ARKitSession和HandTrackingProvider是 ARKit 的核心类型arSession.run([handTracking])启动手部追踪handTracking.anchorUpdates提供异步的手部锚点更新流anchor.chirality区分左右手anchor.skeleton.jointNames和joint(named:).localTransform获取每根手指关节的变换- 将关节旋转同步到虚拟模型的
jointTransforms实现手套跟随真实手部运动
(20:52)
直接启动进入 Space
对于完全沉浸式的应用(如游戏),可以配置应用启动时直接进入 Space:
@main
struct WorldApp: App {
var body: some Scene {
ImmersiveSpace(id: "solar") {
SolarSystem()
}
.immersionStyle(selection: .constant(.full), in: .full)
}
}
需要在 Info.plist 中配置 UIApplicationSceneManifest,将 ImmersiveSpace 设为启动场景。
(19:33)
核心启发
-
做什么:创建一个从窗口进入沉浸式太空体验的观星应用
- 为什么值得做:ImmersiveSpace 让应用可以从普通的 2D 窗口平滑过渡到 360 度星空环境。用户先浏览天文信息,再点击进入沉浸式模式探索太阳系
- 怎么开始:定义 WindowGroup 作为入口,定义 ImmersiveSpace 存放 3D 内容,用
openImmersiveSpace(id:)触发切换
-
做什么:为健身应用添加一个 progressive 沉浸模式的虚拟训练场
- 为什么值得做:progressive 样式让用户可以在保持环境感知的同时享受沉浸式训练。通过 Digital Crown 调节沉浸度,从”看到客厅里的虚拟教练”到”完全进入虚拟健身房”
- 怎么开始:使用
.immersionStyle(selection:in:)支持 mixed 和 progressive,用 MagnifyGesture 或按钮切换样式
-
做什么:开发一个支持手部追踪的虚拟乐器演奏应用
- 为什么值得做:ARKit 手部追踪提供每根手指关节的精确数据,可以检测弹奏动作。配合
.upperLimbVisibility(.hidden)和虚拟手部模型,营造真实演奏感 - 怎么开始:使用
HandTrackingProvider获取手部锚点,将关节变换映射到虚拟乐器模型的交互区域
- 为什么值得做:ARKit 手部追踪提供每根手指关节的精确数据,可以检测弹奏动作。配合
-
做什么:构建一个多人协作的 3D 白板应用
- 为什么值得做:ImmersiveSpace 支持 SharePlay,多个用户可以在同一个空间中看到和操作相同的内容。坐标转换 API 让窗口中的 2D 内容可以精确定位到 3D 空间中
- 怎么开始:集成 GroupActivities 框架,使用
GeometryReader3D和.immersiveSpace坐标空间进行位置转换
-
做什么:制作一个直接启动的冥想/放松应用
- 为什么值得做:full immersion 样式可以完全屏蔽外界干扰,配合
.preferredSurroundingsEffect(.systemDark)营造专注氛围。应用启动即进入体验,无需用户额外操作 - 怎么开始:在 App 声明中只保留 ImmersiveSpace,设置 immersionStyle 为 full,配置 scene manifest 直接启动
- 为什么值得做:full immersion 样式可以完全屏蔽外界干扰,配合
关联 Session
- Meet SwiftUI for spatial computing — SwiftUI spatial computing 入门,了解窗口和 Volume 的基础概念
- Take SwiftUI to the next dimension — 在 SwiftUI 中使用 3D 内容、Model3D 和 RealityView 的更多用法
- Build spatial experiences with RealityKit — RealityKit 深度解析,实体、组件、动画系统详解
- Evolve your ARKit app for spatial experiences — ARKit 在 visionOS 上的手部追踪、平面检测和空间定位
评论
GitHub Issues · utterances