WWDC Quick Look 💓 By SwiftGGTeam
Take SwiftUI to the next dimension

Take SwiftUI to the next dimension

观看原视频

Highlight

SwiftUI 在 visionOS 上通过 Volume 容器、Model3D API、RealityView attachments 和 3D 手势,让开发者可以用熟悉的声明式语法构建有界 3D 体验,无需接管整个空间即可展示和操作三维内容。

核心内容

Volume:固定尺寸的 3D 容器

Window 会根据距离动态缩放,Volume 不同——它保持固定尺寸,无论离你多远,1 米宽的 Volume 始终是 1 米宽。Volume 水平对齐,支持从任意角度观看。

创建 Volume 只需要在 WindowGroup 上应用新的窗口样式:

@main
struct WorldApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .windowStyle(.volumetric)
    }
}

关键点:

  • .windowStyle(.volumetric) 将普通窗口变为 Volume 容器
  • Volume 内没有玻璃背景,3D 内容直接放入场景
  • 内容可以配合控制面板一起展示

02:14

Model3D:3D 资源的 AsyncImage

Model3D 是加载 USDZ 等 3D 资源的 SwiftUI 视图,设计理念与 AsyncImage 类似,通过 phase 管理加载生命周期:

struct MoonView: View {
    var body: some View {
        Model3D(named: "Moon") { phase in
            switch phase {
            case .empty:
                ProgressView()
            case .failure(let error):
                Text(error.localizedDescription)
            case .success(let model):
                model
                    .resizable()
                    .scaledToFit()
            }
        }
    }
}

关键点:

  • Model3D(named:) 从资源包加载指定名称的 USDZ 文件
  • phase 有三种状态:empty(加载中)、failure(失败)、success(成功)
  • .resizable() 告知布局系统模型可以根据可用空间调整大小
  • .scaledToFit() 让模型在可用空间内保持比例缩放

03:35

3D 布局与深度对齐

多个 3D 模型并排时,默认使用背面对齐(back face aligned),即所有模型的背面齐平:

struct CelestialObjectView: View {
    let name: String
    let size: CGFloat

    var body: some View {
        Model3D(named: name) { phase in
            switch phase {
            case .success(let model):
                model
                    .resizable()
                    .scaledToFit()
                    .frame(width: size, height: size, depth: size)
                    .frame(depth: size, alignment: .front)
            default:
                EmptyView()
            }
        }
    }
}

关键点:

  • .frame(width:height:depth:) 设置 3D 视图的三维尺寸
  • .frame(depth:alignment:) 控制深度方向的对齐方式
  • .front 让模型正面对齐,.back(默认)让背面对齐
  • 从斜侧面观看时,对齐方式的差异会很明显

05:43

3D 动画效果

SwiftUI 提供了 Rotation3DEffect 用于三维旋转:

TimelineView(.animation) { timeline in
    CelestialObjectView(name: object.name, size: object.size)
        .rotation3DEffect(
            Angle(degrees: timeline.date.timeIntervalSinceReferenceDate * 10),
            axis: .y
        )
}

关键点:

  • TimelineView(.animation) 提供持续更新的时间源
  • .rotation3DEffect(axis:) 围绕指定轴旋转视图
  • 这里围绕 y 轴旋转,模拟天体自转效果

07:09

详细内容

RealityView Attachments:将 SwiftUI 视图嵌入 3D 场景

RealityView 的 attachments API 让你把 SwiftUI 视图绑定到 RealityKit 实体上。这些视图是活的——可以响应状态变化、运行动画、处理手势。

RealityView { content, attachments in
    // 加载地球模型
    let earth = try! Entity.load(named: "Earth.usdz")
    content.add(earth)

    // 为每个地点创建标签附件
    for place in favoritePlaces {
        if let label = attachments.entity(for: place.id) {
            content.add(label)
            // 将标签定位到地球表面
            label.lookAt(earth, from: place.location, relativeTo: earth)
        }
    }
} attachments: {
    ForEach(favoritePlaces) { place in
        Text(place.name)
            .glassBackgroundEffect()
            .tag(place.id)
    }
}

关键点:

  • attachments 参数提供对标记视图的访问
  • 视图通过 .tag() 标记,在 RealityView 中通过 attachments.entity(for:) 获取对应的实体
  • .glassBackgroundEffect() 确保标签在任何背景下可读
  • lookAt(from:relativeTo:) 将附件定位并朝向指定位置
  • 附件视图支持完整的 SwiftUI 功能,包括状态更新和手势

08:39

为实体配置输入接收

要让 RealityView 中的实体响应手势,需要添加两个组件:

RealityView { content in
    let earth = try! Entity.load(named: "Earth.usdz")

    // 添加输入目标组件,允许接收输入
    earth.components.set(InputTargetComponent())

    // 添加碰撞组件,定义交互区域的形状
    earth.components.set(
        CollisionComponent(shapes: [.generateSphere(radius: 1.0)])
    )

    content.add(earth)
}

关键点:

  • InputTargetComponent() 标记实体及其后代可以接收输入
  • CollisionComponent 定义交互区域的几何形状
  • 这里使用球体碰撞体匹配地球模型的形状
  • 碰撞体精度影响手势定位的准确性

11:32

空间点击手势

SpatialTapGesture 提供 3D 点击位置:

RealityView { content in
    // ... 配置地球实体
}
.gesture(
    SpatialTapGesture()
        .targetedToEntity(earthEntity)
        .onEnded { value in
            // 获取 3D 点击位置(SwiftUI 本地坐标空间,单位是 points)
            let location3D = value.location3D

            // 转换为场景坐标空间
            let sceneLocation = value.convert(location3D, from: .local, to: earthEntity)

            // 在点击位置上方添加新地点
            let newPlace = FavoritePlace(
                name: "New Discovery",
                location: sceneLocation * 1.05 // 稍微浮出表面
            )
            favoritePlaces.append(newPlace)
        }
)

关键点:

  • .targetedToEntity() 将手势限定在指定实体上,点击其他区域手势失败
  • value.location3D 获取点击的 3D 坐标
  • value.convert(...) 在坐标空间之间转换位置
  • 坐标单位是 points,需要转换为场景空间使用

12:15

组合 3D 操作手势

将拖拽、缩放和旋转组合成一个完整的手势:

var manipulationGesture: some Gesture<AffineTransform3D> {
    DragGesture()
        .simultaneously(with: MagnifyGesture())
        .simultaneously(with: RotateGesture3D())
        .map { gesture in
            let (translation, scale, rotation) = gesture.components()
            return AffineTransform3D(
                scale: scale,
                rotation: rotation,
                translation: translation
            )
        }
}

// 提取手势组件的辅助方法
extension SimultaneousGesture<
    SimultaneousGesture<DragGesture, MagnifyGesture>,
    RotateGesture3D
>.Value {
    func components() -> (Vector3D, Size3D, Rotation3D) {
        let translation = self.first?.first?.translation3D ?? .zero
        let magnification = self.first?.second?.magnification ?? 1
        let size = Size3D(
            width: magnification,
            height: magnification,
            depth: magnification
        )
        let rotation = self.second?.rotation ?? .identity
        return (translation, size, rotation)
    }
}

使用手势控制实体变换:

@State private var isDragging = false

RealityView { content, attachments in
    // ... 配置内容
} update: { content, attachments in
    if let satellite = content.entities.first(where: { $0.name == "satellite" }) {
        satellite
            .gesture(
                manipulationGesture
                    .updating($isDragging) { _, state, _ in
                        state = true
                    }
                    .onChanged { value in
                        satellite.transform = Transform(value)
                    }
            )
    }
}

关键点:

  • DragGesturetranslation3D 属性提供 3D 平移量
  • RotateGesture3D 测量无约束的 3D 旋转
  • simultaneously(with:) 将多个手势组合为同时识别的手势组
  • AffineTransform3D 封装 3D 变换的平移、旋转和缩放
  • .updating 跟踪手势的瞬时状态,手势失败时自动重置
  • 手势支持直接手部交互、间接捏合、触控板和辅助功能

17:26

核心启发

  • 做什么:创建一个 3D 产品展示应用,让用户可以从任意角度查看商品

    • 为什么值得做:Volume 容器让 3D 内容可以在 Shared Space 中与其他应用共存。Model3D 异步加载 USDZ 模型,配合 .resizable().scaledToFit() 自动适配布局
    • 怎么开始:用 .windowStyle(.volumetric) 创建 Volume,用 Model3D 加载产品模型,添加 Rotation3DEffect 让模型自动旋转展示
  • 做什么:开发一个带标注的 3D 地图或解剖图应用

    • 为什么值得做:RealityView attachments 让 SwiftUI 视图可以精确附着在 3D 模型的特定位置。标签可以响应状态更新,支持玻璃背景确保可读性
    • 怎么开始:在 RealityView 的 attachments 闭包中创建 Text 标签,用 .tag() 标记,在 RealityView 闭包中用 attachments.entity(for:) 获取并定位到模型表面
  • 做什么:构建一个支持手势操作的 3D 设计工具

    • 为什么值得做:组合手势(拖拽+缩放+旋转)让用户可以用自然的手部动作操纵 3D 对象。targetedToEntity 确保手势精确作用于目标对象
    • 怎么开始:定义 DragGesture.simultaneously(with: MagnifyGesture).simultaneously(with: RotateGesture3D) 组合手势,用 .targetedToEntity() 限定目标,在 .onChanged 中更新实体的 transform
  • 做什么:制作一个 3D 拼图或组装教学应用

    • 为什么值得做:SpatialTapGesture 的 location3D 提供精确的 3D 点击位置。通过 InputTargetComponentCollisionComponent 可以为复杂模型配置准确的交互区域
    • 怎么开始:为每个可交互部件添加碰撞组件,使用 SpatialTapGesture().targetedToEntity() 捕获点击,根据点击位置判断用户选择了哪个部件

关联 Session

评论

GitHub Issues · utterances