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 ↗Chapters
Code shown on screen · 22 snippets
Sparky in Model3D
struct ContentView: View {
var body: some View {
Model3D(named: "sparky")
}
} Sparky in Model3D with a name sign
struct ContentView: View {
var body: some View {
HStack {
NameSign()
Model3D(named: "sparky")
}
}
} Display a model asset in a Model3D and present playback controls
struct RobotView: View {
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
struct RobotView: View {
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
struct RobotView: View {
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
struct RobotAnimationControls: View {
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
struct ConfigCatalogExample: View {
private var configCatalog: Entity.ConfigurationCatalog?
private var configurations = [String: String]()
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
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
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
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
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
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
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
struct RobotView: View {
let url: URL
var body: some View {
HStack {
NameSign()
Model3D(url: url)
.manipulable(inertia: .high)
}
}
} Add a ManipulationComponent to an entity
RealityView { content in
let sparky = await loadSparky()
content.add(sparky)
ManipulationComponent.configureEntity(sparky)
} Add a ManipulationComponent to an entity with configuration
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
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
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
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
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
struct AttachmentComponentAttachments: View {
private var bolts = Entity()
private var nameSign = Entity()
var body: some View {
RealityView { ... }
.realityViewLayoutBehavior(.centered)
.gesture(
TapGesture()
.targetedToEntity(bolts)
.onEnded { value in
nameSign.isEnabled.toggle()
}
)
}
} Gestures with GestureComponent
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
Related sessions
-
35 min -
25 min -
13 min -
36 min