Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2025 DesignSpatial ComputingSwiftUI & UI Frameworks

WWDC25 · 30 min · Design / Spatial Computing / SwiftUI & UI Frameworks

Better together: SwiftUI and RealityKit

Discover how to seamlessly blend SwiftUI and RealityKit in visionOS 26. We’ll explore enhancements to Model3D, including animation and ConfigurationCatalog support, and demonstrate smooth transitions to RealityView. You’ll learn how to leverage SwiftUI animations to drive RealityKit component changes, implement interactive manipulation, use new SwiftUI components for richer interactions, and observe RealityKit changes from your SwiftUI code. We’ll also cover how to use unified coordinate conversion for cross-framework coordinate transformations.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

Code shown on screen · 22 snippets

Sparky in Model3D swift · at 1:42 ↗
struct ContentView: View {
  var body: some View {
    Model3D(named: "sparky")
  }
}
Sparky in Model3D with a name sign swift · at 1:52 ↗
struct ContentView: View {
  var body: some View {
    HStack {
      NameSign()
      Model3D(named: "sparky")
    }
  }
}
Display a model asset in a Model3D and present playback controls​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​ swift · at 3:18 ↗
struct RobotView: View {
  @State private var asset: Model3DAsset?
  var body: some View {
    if asset == nil {
      ProgressView().task { asset = try? await Model3DAsset(named: "sparky") }
    }
  }
}
Display a model asset in a Model3D and present playback controls​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​ swift · at 3:34 ↗
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)
      }
    }
  }
}
Display a model asset in a Model3D and present playback controls​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​ swift · at 4:03 ↗
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)
        }
      }
    }
  }
}
Pause, resume, stop, and change the move the play head in the animation​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​ swift · at 4: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)
    }
  }
}
Load a Model3D using a ConfigurationCatalog​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​ swift · at 5:41 ↗
struct ConfigCatalogExample: View {
  @State private var configCatalog: Entity.ConfigurationCatalog?
  @State private var configurations = [String: String]()
  @State private var showConfig = false
  var body: some View {
    if let configCatalog {
      Model3D(from: configCatalog, configurations: configurations)
        .popover(isPresented: $showConfig, arrowEdge: .leading) {
          ConfigPicker(
            name: "outfits",
            configCatalog: configCatalog,
            chosenConfig: $configurations["outfits"])
        }
    } else {
      ProgressView()
        .task {
          await loadConfigurationCatalog()
        }
    }
  }
}
Switching from Model3D to RealityView swift · at 6:51 ↗
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)
        }
      }
    }
  }
}
Switching from Model3D to RealityView with layout behavior swift · at 7:25 ↗
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)
    }
  }
}
Switching from Model3D to RealityView with layout behavior and RealityKit animation swift · at 8:48 ↗
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)
          sparky.playAnimation(getAnimation())
        }
      }
      .realityViewLayoutBehavior(.fixedSize)
    }
  }
}
Add 2 particle emitters; one to each side of the robot's head swift · at 10:34 ↗
func setupSparks(robotHead: Entity) {
  let leftSparks = Entity()
  let rightSparks = Entity()

  robotHead.addChild(leftSparks)
  robotHead.addChild(rightSparks)

  rightSparks.components.set(sparksComponent())
  leftSparks.components.set(sparksComponent())

  leftSparks.transform.rotation = simd_quatf(Rotation3D(
    angle: .degrees(180),
    axis: .y))

  leftSparks.transform.translation = leftEarOffset()
  rightSparks.transform.translation = rightEarOffset()
}

// Create and configure the ParticleEmitterComponent
func sparksComponent() -> ParticleEmitterComponent { ... }
Apply the manipulable view modifier swift · at 12:30 ↗
struct RobotView: View {
  let url: URL
  var body: some View {
    HStack {
      NameSign()
      Model3D(url: url)
        .manipulable()
    }
  }
}
Allow translate, 1- and 2-handed rotation, but not scaling swift · at 12:33 ↗
struct RobotView: View {
  let url: URL
  var body: some View {
    HStack {
      NameSign()
      Model3D(url: url)
        .manipulable(
          operations: [.translation,
                       .primaryRotation,
                       .secondaryRotation]
       )
    }
  }
}
The model feels heavy with high inertia swift · at 12:41 ↗
struct RobotView: View {
  let url: URL
  var body: some View {
    HStack {
      NameSign()
      Model3D(url: url)
        .manipulable(inertia: .high)
    }
  }
}
Add a ManipulationComponent to an entity​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​ swift · at 13:18 ↗
RealityView { content in
  let sparky = await loadSparky()
  content.add(sparky)
  ManipulationComponent.configureEntity(sparky)
}
Add a ManipulationComponent to an entity​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​ with configuration swift · at 13:52 ↗
RealityView { content in
  let sparky = await loadSparky()
  content.add(sparky)
  ManipulationComponent.configureEntity(
    sparky,
    hoverEffect: .spotlight(.init(color: .purple)),
    allowedInputTypes: .all,
    collisionShapes: myCollisionShapes()
  )
}
Manipulation interaction events swift · at 14:08 ↗
public enum ManipulationEvents {

  /// When an interaction is about to begin on a ManipulationComponent's entity
  public struct WillBegin: Event { }
  
  /// When an entity's transform was updated during a ManipulationComponent
  public struct DidUpdateTransform: Event { }

  /// When an entity was released
  public struct WillRelease: Event { }

  /// When the object has reached its destination and will no longer be updated
  public struct WillEnd: Event { }

  /// When the object is directly handed off from one hand to another
  public struct DidHandOff: Event { }
}
Replace the standard sounds with custom ones swift · at 14:32 ↗
RealityView { content in
  let sparky = await loadSparky()
  content.add(sparky)

  var manipulation = ManipulationComponent()
  manipulation.audioConfiguration = .none
  sparky.components.set(manipulation)

  didHandOff = content.subscribe(to: ManipulationEvents.DidHandOff.self) { event in
    sparky.playAudio(handoffSound)
  }
}
Builder based attachments swift · at 16:19 ↗
struct RealityViewAttachments: View {
  var body: some View {
    RealityView { content, attachments in
      let bolts = await loadAndSetupBolts()
      if let nameSign = attachments.entity(
        for: "name-sign"
      ) {
        content.add(nameSign)
        place(nameSign, above: bolts)
      }
      content.add(bolts)
    } attachments: {
      Attachment(id: "name-sign") {
        NameSign("Bolts")
      }
    }
    .realityViewLayoutBehavior(.centered)
  }
}
Attachments created with ViewAttachmentComponent swift · at 16:37 ↗
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)
      content.add(bolts)
      content.add(nameSign)
    }
    .realityViewLayoutBehavior(.centered)
  }
}
Targeted to entity gesture API swift · at 17:04 ↗
struct AttachmentComponentAttachments: View {
  @State private var bolts = Entity()
  @State private var nameSign = Entity()

  var body: some View {
    RealityView { ... }
    .realityViewLayoutBehavior(.centered)
    .gesture(
      TapGesture()
        .targetedToEntity(bolts)
        .onEnded { value in
          nameSign.isEnabled.toggle()
        }
    )
  }
}
Gestures with GestureComponent swift · at 17: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)
  }
}

Resources