WWDC Quick Look 💓 By SwiftGGTeam
Better together: SwiftUI and RealityKit

Better together: SwiftUI and RealityKit

观看原视频

Highlight

visionOS 26 让 SwiftUI 和 RealityKit 真正合体:Model3D 直接播放动画、RealityKit Entity 接住 SwiftUI 视图与手势、坐标系双向贯通。

核心内容

过去在 visionOS 上展示一个 3D 机器人,开发者经常被迫在 Model3D 和 RealityView 之间二选一。Model3D 一行代码就能塞进 SwiftUI 布局,但播不了动画、加不了粒子;想要换装、加碰撞、做粒子发射,就得切到 RealityView,然后立刻面对一个新麻烦——RealityView 默认会把所有可用空间吃干净,旁边并排的 NameSign 立刻被推到屏幕一边。

visionOS 26 把这条路彻底打通。Apple 给 Model3D 加上了 Model3DAsset,可以直接读取模型自带的动画轨道,配合新的 ConfigurationCatalog 切换不同外观;想升级到 RealityView 时,新加的 .realityViewLayoutBehavior(.fixedSize) 让它像 Model3D 一样紧贴模型边界。RealityKit 这边也开放了三个 SwiftUI 风格的新组件——ViewAttachmentComponent、GestureComponent、PresentationComponent,外加新的 ManipulationComponent 让用户用单手或双手抓起虚拟物体。Entity 现在是 Observable 的,AnimationPlaybackController 也是,SwiftUI 视图可以直接 @Bindable 这些 RealityKit 对象,状态在两边自由流动。

详细内容

Model3D 加载动画与控制器03:34)。Model3DAsset 异步加载模型文件,再交给 Model3D 显示;选定的动画通过 animationPlaybackController 暴露出来,配合 SwiftUI 的 Slider 做时间擦除。

struct RobotView: View {
  @State private var asset: Model3DAsset?
  var body: some View {
    if asset == nil {
      ProgressView().task { asset = try? await Model3DAsset(named: "sparky") }
    } else if let asset {
      VStack {
        Model3D(asset: asset)
        AnimationPicker(asset: asset)
        if let animationController = asset.animationPlaybackController {
          RobotAnimationControls(playbackController: animationController)
        }
      }
    }
  }
}

关键点:

  • Model3DAsset(named:) 是异步初始化器,要放在 .task 里等待。
  • asset == nil 阶段用 ProgressView 兜底,加载完才切到真正的 Model3D。
  • asset.animationPlaybackController 由 Model3DAsset 在选中动画后生成,外部不要自己 new。
  • AnimationPlaybackController 在 visionOS 26 已经是 Observable,可直接当 view model 用。

用 @Bindable 把 SwiftUI 控件接到 RealityKit 控制器04:32)。

struct RobotAnimationControls: View {
  @Bindable var controller: AnimationPlaybackController

  var body: some View {
    HStack {
      Button(controller.isPlaying ? "Pause" : "Play") {
        if controller.isPlaying { controller.pause() }
        else { controller.resume() }
      }

      Slider(
        value: $controller.time,
        in: 0...controller.duration
      ).id(controller)
    }
  }
}

关键点:

  • @Bindable var controller 把 RealityKit 类直接挂成 SwiftUI 的可观察数据源。
  • controller.isPlaying 变化会触发视图重算,按钮文案自动同步。
  • $controller.time 把 Slider 的拖动直接写回控制器,做到时间擦除。
  • .id(controller) 在动画切换时强制 Slider 重建,避免上一个动画的范围残留。

从 Model3D 平滑迁移到 RealityView07:25)。RealityView 默认拿走全部可用空间,加上 .fixedSize 后会用首次布局时 Entity 的视觉边界来定尺寸。

struct RobotView: View {
  let url: URL = Bundle.main.url(forResource: "sparky", withExtension: "reality")!

  var body: some View {
    HStack {
      NameSign()
      RealityView { content in
        if let sparky = try? await Entity(contentsOf: url) {
          content.add(sparky)
        }
      }
      .realityViewLayoutBehavior(.fixedSize)
    }
  }
}

关键点:

  • .fixedSize.flexible.centered 三选一,分别对应紧贴边界、占满空间、居中放置。
  • 尺寸只在 make 闭包结束后评估一次,后续 Entity 体积变化不会重新撑大 RealityView。
  • 这三个修饰符只移动 RealityView 的原点,不会改写 Entity 自身的位置或缩放。

ManipulationComponent 让 Entity 被双手抓取13:18)。configureEntity 一行代码会同时挂上 ManipulationComponent、CollisionComponent、InputTargetComponent、HoverEffectComponent。

RealityView { content in
  let sparky = await loadSparky()
  content.add(sparky)
  ManipulationComponent.configureEntity(
    sparky,
    hoverEffect: .spotlight(.init(color: .purple)),
    allowedInputTypes: .all,
    collisionShapes: myCollisionShapes()
  )
}

关键点:

  • configureEntity 是静态便利方法,自动补齐交互所需的全部组件。
  • hoverEffect 控制凝视/悬停时的视觉反馈,这里用紫色聚光灯。
  • allowedInputTypes: .all 同时允许直接触碰和 gaze + pinch 间接输入。
  • collisionShapes 决定可抓取范围,传 nil 则用自动生成。
  • 想换抓取/放下的音效,把 audioConfiguration 设为 .none,再订阅 ManipulationEvents.DidHandOff 自行播放。

ViewAttachmentComponent + GestureComponent 让 Entity 长出 SwiftUI17:10)。

struct AttachmentComponentAttachments: View {
  var body: some View {
    RealityView { content in
      let bolts = await loadAndSetupBolts()
      let attachment = ViewAttachmentComponent(
          rootView: NameSign("Bolts"))
      let nameSign = Entity(components: attachment)
      place(nameSign, above: bolts)
      bolts.components.set(GestureComponent(
        TapGesture().onEnded {
          nameSign.isEnabled.toggle()
        }
      ))
      content.add(bolts)
      content.add(nameSign)
    }
    .realityViewLayoutBehavior(.centered)
  }
}

关键点:

  • ViewAttachmentComponent 直接用任意 SwiftUI 视图作 rootView,不再需要 RealityView 初始化时声明 attachments。
  • Entity(components: attachment) 把组件作为初始化参数注入,省掉一次 set。
  • GestureComponent 接收标准 SwiftUI 手势,回调里的坐标默认就是 Entity 的本地空间。
  • 用 GestureComponent 时,目标 Entity 仍需要 InputTargetComponent 和 CollisionComponent,否则收不到事件。

核心启发

  • 做什么:把现有 Model3D 升级成 Model3DAsset 驱动的播放器

    • 为什么值得做:原本静态展示的 3D 资源能直接复用美术导出的动画,用户能换装、能拖时间轴。
    • 怎么开始:把模型导出为带动画的 reality 文件,用 Model3DAsset(named:) 加载,配上 Picker + Slider 做最小可玩 demo。
  • 做什么:在 visionOS 应用里用 .realityViewLayoutBehavior(.fixedSize) 替换”撑满空间”的 RealityView

    • 为什么值得做:很多布局错位问题都源于 RealityView 默认抢空间,加上一个修饰符就能让它和周围 SwiftUI 控件并排。
    • 怎么开始:搜索项目里所有 RealityView,把展示型用例都加上 .fixedSize,让交互式场景保留 .flexible
  • 做什么:给可被抓取的 3D 物体加一行 ManipulationComponent.configureEntity

    • 为什么值得做:四个常用组件一次配齐,加上系统提供的握取音效,几乎零成本就能让虚拟物体响应双手交互。
    • 怎么开始:先在原型里对核心展示物体启用,订阅 ManipulationEvents.DidUpdateTransform 把位姿写回业务状态。
  • 做什么:用 ViewAttachmentComponent 把信息卡、按钮、说明文字直接挂在 Entity 上

    • 为什么值得做:SwiftUI 视图作为组件挂载之后,可以跟随 Entity 一起被场景管理、被手势响应、被 Observable 状态驱动。
    • 怎么开始:把当前用 RealityViewAttachments 写的 nameSign / tooltip 改成 ViewAttachmentComponent,再尝试挂上 GestureComponent 让它可点。

关联 Session

评论

GitHub Issues · utterances