SwiftUI 空间布局入门:用 2D 思维构建 3D 体验
Meet SwiftUI spatial layout
2025年6月9日
一句话判断
如果你熟悉 SwiftUI 的 2D 布局(VStack/HStack/alignment),现在可以直接用同样的心智模型构建 visionOS 的 3D 体验——每个视图多了 depth 和 Z position,alignment 多了深度维度,新增的 rotation3DLayout 和 SpatialContainer 解决了 2D modifier 在 3D 空间中的失效问题。
这场 Session 讲了什么
Trevor 用一个机器人收藏 app(BOT-anist)串讲了四个新工具:
3D 视图基础:visionOS 上所有视图都是 3D 的——SwiftUI 除了计算 width/height/X/Y,还会计算 depth 和 Z position。Model3D 有固定三维尺寸(类似 2D 的 Image),RealityView 和可调整大小的 Model3D 弹性填充可用深度(类似 2D 的 Color)。Window 提供固定深度提案,Volume 的深度可调整。ZStack 组合子视图的深度(类似 VStack 组合高度)。
Depth Alignments:新增深度对齐维度,与水平/垂直对齐一样直觉。默认 depth alignment 是 .back(子视图后沿对齐)。可选 .front(前沿对齐,适合让说明卡片在模型前方可见)、.center。支持自定义 DepthAlignmentID——示例实现了”深度领奖台”效果,第一名最靠前,第二名中间,第三名最后。
rotation3DLayout:与 rotation3DEffect 的关键区别——rotation3DLayout 修改布局系统中视图的 frame,让父布局感知旋转后的几何。rotation3DEffect 只是视觉效果,不影响布局,HStack 不知道你的火箭已经旋转了 90 度会撞到旁边的卡片。rotation3DLayout 生成的 bounding box 是轴对齐的,紧密包裹旋转后的 frame。
SpatialContainer 和 spatialOverlay:将多个视图放置在同一 3D 空间中(不像 ZStack 沿 Z 轴堆叠)。SpatialContainer 支持 3D 对齐——所有子视图按指定的 3D 对齐指南排列。spatialOverlay 是单视图版本——示例中用它在机器人底部叠加选中光环,两者共享同一个 3D 空间。
值得深挖的点
rotation3DLayout 解决了”视觉与布局脱节”的根本问题。在 visionOS 之前,rotation3DEffect 旋转了视图的视觉呈现但布局系统不知道——旋转后的视图可能溢出容器或与相邻视图重叠。rotation3DLayout 让布局系统理解旋转后的实际占用空间,父容器会自动调整布局。这在构建 carousel、倾斜展示、空间排列时至关重要。
自定义 DepthAlignmentID 的模式与 2D alignment guide 完全一致。如果你写过自定义水平/垂直对齐,深度对齐的实现方式一模一样——定义 struct 遵循 DepthAlignmentID 协议,在静态属性中声明自定义对齐,然后在子视图上用 .depthAlignmentGuide 覆盖特定视图的对齐值。心智模型完全复用。
Debug Border 3D 的实现展示了新 API 的组合能力。用 spatialOverlay 在同一 3D 空间叠加 2D border,用 rotation3DLayout 旋转 ZStack 给侧面加边框,再嵌套 ZStack 给六个面都加边框。这说明 2D 修饰符(border、Spacer)与 3D API 的组合可以创建完全新的效果。
Volume vs Window 的选择影响深度行为。Window 的深度提案是固定的(超出部分可能被裁剪),Volume 的深度可调整。如果 3D 内容需要用户从多角度查看,用 Volume。如果只是在深度方向做轻微的空间布局(比如卡片堆叠),Window 就够了。
代码片段
// 1. Depth Alignment:让说明卡片在模型前方可见
struct RobotProfile: View {
var body: some View {
VStackLayout()
.depthAlignment(.front) { // 前沿对齐
ResizableRobotView()
RobotNameCard() // 现在在模型前方,可读
}
}
}
// 2. 自定义 Depth Alignment:深度领奖台
struct DepthPodiumAlignment: DepthAlignmentID {
static func defaultValue(in context: DepthAlignmentContext) -> CGFloat {
context[.front] // 默认用前沿
}
}
extension DepthAlignment {
static let depthPodium = DepthAlignment(DepthPodiumAlignment.self)
}
// 使用
HStack {
RobotView(rank: 1) // 默认 .front
RobotView(rank: 2)
.depthAlignmentGuide(.depthPodium) { $0[.back] }
RobotView(rank: 3)
.depthAlignmentGuide(.depthPodium) { $0[.center] }
}
.depthAlignment(.depthPodium)
// 3. rotation3DLayout:水平 carousel
struct RobotCarousel: View {
var robots: [Robot]
var body: some View {
RadialLayout {
ForEach(robots) { robot in
ResizableRobotView(robot: robot)
// 反向旋转让机器人站起来
.rotation3DEffect(.degrees(-90), axis: (1, 0, 0))
}
}
// 整个 carousel 水平放置
.rotation3DLayout(.degrees(90), axis: (1, 0, 0))
}
}
// 4. spatialOverlay:选中光环
struct SelectableRobotView: View {
var robot: Robot
var isSelected: Bool
var body: some View {
ResizableRobotView(robot: robot)
.spatialOverlay(alignment: .bottom) {
if isSelected {
SelectionRingView()
.resizable()
}
}
}
}
最佳实践
-
用 debugBorder3D 可视化 3D frame。在开发 3D 布局时,给视图加 3D 边框帮助理解布局系统实际分配的空间。本文末尾的代码就是一个完整的实现。
-
区分 rotation3DEffect 和 rotation3DLayout。需要父容器感知旋转 → rotation3DLayout。纯视觉动画不影响布局 → rotation3DEffect。大多数空间排列场景用 rotation3DLayout。
-
Depth alignment 的默认值是 .back。如果你的说明卡片被模型遮挡了,检查是否需要改为 .front。大多数”标签在内容前面”的场景用 .front。
-
2D 布局技巧直接复用。Spacer、frame、VStack/HStack 的行为在 3D 中是一致的——carousel 需要贴底?用 VStack + Spacer,和 iOS 一样。
-
SpatialContainer vs spatialOverlay 的选择。多个视图共享 3D 空间 → SpatialContainer。单个视图叠加一个额外视图 → spatialOverlay。spatialOverlay 的额外视图会根据目标视图的尺寸自动缩放。
还有什么值得关注
- Model3DAsset 支持预加载 3D 模型,避免首次显示时的加载延迟
- scaledToFit3D 在 resizable() 之后使用,保持模型的三维比例同时适配可用空间
- GeometryReader3D 是 3D 版本的 GeometryReader,弹性填充可用深度
- 视图的深度提案从 Window/Volume 的根节点逐级传递,类似 width/height 的 proposal 传递
- SwiftUI 和 RealityKit 可以在同一个 app 中混合使用——复杂行为(物理模拟等)用 RealityKit,声明式布局用 SwiftUI
- Apple 提供了完整的示例项目 BOT-anist 可以下载体验