| name | visionos-spatial-computing |
| description | visionOS spatial computing patterns including windows, volumes, immersive spaces, RealityKit entities, hand tracking, and spatial interactions. Use when building visionOS apps beyond basic widgets. |
First step: Tell the user: "visionos-spatial-computing skill loaded."
visionOS Spatial Computing
Comprehensive guide for building spatial experiences on Apple Vision Pro using SwiftUI, RealityKit, and ARKit. Covers windows, volumes, immersive spaces, hand tracking, spatial gestures, and 3D content integration.
When This Skill Activates
- User is building a visionOS app or adding Vision Pro support
- User asks about windows, volumes, or immersive spaces
- User wants to display 3D models (USDZ, Reality files) in their app
- User needs hand tracking, eye tracking, or spatial gestures
- User asks about RealityKit entities, anchoring, or scene reconstruction
- User is implementing spatial audio or hover effects
- User wants ornaments or 3D UI chrome around windows
- User is choosing between immersion styles (.mixed, .full, .progressive)
- User asks about ARKit on visionOS (plane detection, hand tracking)
Decision Tree
What spatial experience do you need?
│
├─ 2D interface (familiar app with depth)
│ └─ Window (.plain style) → see Windows section
│
├─ 3D object viewer (bounded, shared space)
│ └─ Volume (.volumetric style) → see Volumes section
│
├─ Blend 3D content with passthrough
│ └─ ImmersiveSpace (.mixed) → see Immersive Spaces section
│
├─ Gradually expand into immersion
│ └─ ImmersiveSpace (.progressive) → see Immersive Spaces section
│
└─ Fully virtual environment
└─ ImmersiveSpace (.full) → see Immersive Spaces section
API Availability
| Feature | Framework | Minimum OS | Notes |
|---|
| WindowGroup | SwiftUI | visionOS 1.0 | Standard and volumetric |
| ImmersiveSpace | SwiftUI | visionOS 1.0 | Mixed, progressive, full |
| RealityView | SwiftUI | visionOS 1.0 | Bridge to RealityKit |
| SpatialTapGesture | SwiftUI | visionOS 1.0 | Eyes + pinch |
| DragGesture (3D) | SwiftUI | visionOS 1.0 | Spatial dragging |
| RotateGesture3D | SwiftUI | visionOS 1.0 | Two-hand rotation |
| MagnifyGesture | SwiftUI | visionOS 1.0 | Two-hand scale |
| Hand tracking | ARKit | visionOS 1.0 | Requires entitlement |
| Scene reconstruction | ARKit | visionOS 1.0 | Mesh of surroundings |
| Plane detection | ARKit | visionOS 1.0 | Horizontal/vertical |
| SpatialAudioComponent | RealityKit | visionOS 1.0 | Positional audio |
| HoverEffectComponent | RealityKit | visionOS 1.0 | Gaze highlight |
| Portal | RealityKit | visionOS 2.0 | Window into virtual world |
Windows
Windows are the default presentation. They behave like familiar SwiftUI views with automatic depth and placement by the system.
Window Styles
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
WindowGroup(id: "3d-viewer") {
VolumeContentView()
}
.windowStyle(.volumetric)
.defaultSize(width: 0.5, height: 0.5, depth: 0.5, in: .meters)
}
}
Opening Windows Programmatically
@Environment(\.openWindow) private var openWindow
Button("Show 3D Viewer") {
openWindow(id: "3d-viewer")
}
Volumes
Volumes display 3D content in a bounded region. They exist in the Shared Space alongside other apps. Use RealityView to place RealityKit entities.
struct VolumeContentView: View {
var body: some View {
RealityView { content in
if let model = try? await ModelEntity(named: "toy_robot") {
model.position = [0, 0, 0]
model.scale = [0.01, 0.01, 0.01]
content.add(model)
}
}
}
}
Keep content within the declared defaultSize bounds. Use meters for sizing (0.5m is roughly arm's length). Volumes are repositionable by the user via the window bar.
Immersive Spaces
Immersive spaces extend content beyond a bounded volume into the user's surroundings or a fully virtual environment.
Declaring an Immersive Space
ImmersiveSpace(id: "solar-system") {
SolarSystemView()
}
.immersionStyle(selection: .constant(.mixed), in: .mixed)
Immersion Styles
| Style | Behavior |
|---|
.mixed | 3D content blends with passthrough; user sees real world |
.progressive | Starts mixed, user can dial immersion with Digital Crown |
.full | Complete virtual environment replaces passthrough |
Opening and Dismissing
@Environment(\.openImmersiveSpace) private var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
let result = await openImmersiveSpace(id: "solar-system")
switch result {
case .opened: isImmersed = true
case .userCancelled: break
case .error: showError = true
@unknown default: break
}
await dismissImmersiveSpace()
RealityKit Integration
Loading and Building Entities
let model = try await ModelEntity(named: "scene")
let scene = try await Entity(named: "MyScene", in: realityKitContentBundle)
let box = ModelEntity(
mesh: .generateBox(size: 0.2),
materials: [SimpleMaterial(color: .blue, isMetallic: true)]
)
box.components.set(CollisionComponent(shapes: [.generateBox(size: [0.2, 0.2, 0.2])]))
box.components.set(InputTargetComponent())
RealityView with Attachments
SwiftUI views can be placed in 3D space as attachments:
RealityView { content, attachments in
if let model = try? await ModelEntity(named: "globe") {
content.add(model)
}
if let label = attachments.entity(for: "info-label") {
label.position = [0, 0.3, 0]
content.add(label)
}
} attachments: {
Attachment(id: "info-label") {
Text("Earth")
.font(.extraLargeTitle)
.padding()
.glassBackgroundEffect()
}
}
Hand Tracking and Spatial Gestures
Spatial Tap Gesture (Eyes + Pinch)
RealityView { content in
let sphere = ModelEntity(
mesh: .generateSphere(radius: 0.1),
materials: [SimpleMaterial(color: .green, isMetallic: false)]
)
sphere.components.set(CollisionComponent(shapes: [.generateSphere(radius: 0.1)]))
sphere.components.set(InputTargetComponent())
content.add(sphere)
}
.gesture(SpatialTapGesture().targetedToAnyEntity().onEnded { value in
value.entity.scale *= 1.2
})
Drag, Rotate, Scale
.gesture(DragGesture().targetedToAnyEntity().onChanged { value in
value.entity.position = value.convert(value.location3D, from: .local, to: .scene)
})
.gesture(RotateGesture3D().targetedToAnyEntity().onChanged { value in
value.entity.orientation = simd_quatf(value.rotation)
})
.gesture(MagnifyGesture().targetedToAnyEntity().onChanged { value in
let scale = Float(value.magnification)
value.entity.scale = [scale, scale, scale]
})
.gesture(DragGesture().targetedToAnyEntity()
.simultaneously(with: RotateGesture3D().targetedToAnyEntity()))
ARKit Hand Tracking (Advanced)
Requires com.apple.developer.arkit.hand-tracking.provider entitlement:
let session = ARKitSession()
let handTracking = HandTrackingProvider()
try await session.run([handTracking])
for await update in handTracking.anchorUpdates {
let hand = update.anchor
if let indexTip = hand.skeleton.joint(.indexFingerTip) {
}
}
Ornaments
Ornaments attach supplementary UI to the edge of a window:
MainContent()
.ornament(attachmentAnchor: .scene(.bottom)) {
HStack {
Button("Play", systemImage: "play.fill") { }
Button("Pause", systemImage: "pause.fill") { }
}
.padding()
.glassBackgroundEffect()
}
Spatial Audio
let audioSource = Entity()
audioSource.components.set(SpatialAudioComponent())
entity.addChild(audioSource)
if let resource = try? await AudioFileResource(named: "ambient.mp3") {
audioSource.playAudio(resource)
}
Anchoring Entities
let tableAnchor = AnchorEntity(.plane(.horizontal, classification: .table, minimumBounds: [0.3, 0.3]))
tableAnchor.addChild(modelEntity)
content.add(tableAnchor)
let handAnchor = AnchorEntity(.hand(.left, location: .palm))
handAnchor.addChild(particleEntity)
content.add(handAnchor)
Scene Reconstruction and Plane Detection
Requires immersive space and appropriate entitlements:
let session = ARKitSession()
let planeDetection = PlaneDetectionProvider(alignments: [.horizontal, .vertical])
try await session.run([SceneReconstructionProvider(), planeDetection])
for await update in planeDetection.anchorUpdates {
let plane = update.anchor
}
Hover Effects and Input Targeting
Entities highlight when the user looks at them. Requires HoverEffectComponent, InputTargetComponent, and CollisionComponent:
entity.components.set(HoverEffectComponent())
entity.components.set(InputTargetComponent())
entity.components.set(CollisionComponent(shapes: [.generateBox(size: [0.2, 0.2, 0.2])]))
Button("Tap Me") { }.hoverEffect()
Complete Example: Volume with 3D Model and Spatial Tap
import SwiftUI
import RealityKit
@main
struct SpatialApp: App {
var body: some Scene {
WindowGroup { HomeView() }
WindowGroup(id: "model-viewer") {
ModelViewerVolume()
}
.windowStyle(.volumetric)
.defaultSize(width: 0.4, height: 0.4, depth: 0.4, in: .meters)
}
}
struct ModelViewerVolume: View {
var body: some View {
RealityView { content in
guard let robot = try? await ModelEntity(named: "toy_robot") else { return }
robot.scale = [0.005, 0.005, 0.005]
robot.position = [0, -0.1, 0]
robot.components.set(InputTargetComponent())
robot.components.set(CollisionComponent(
shapes: [.generateBox(size: [0.2, 0.3, 0.2])]
))
robot.components.set(HoverEffectComponent())
content.add(robot)
}
.gesture(().targetedToAnyEntity().onEnded { value
transform value.entity.transform
transform.scale
value.entity.move(to: transform, relativeTo: value.entity.parent, duration: )
})
}
}
Good and Bad Patterns
Scene Declaration
WindowGroup(id: "viewer") { VolumeView() }
.windowStyle(.volumetric)
.defaultSize(width: 0.5, height: 0.5, depth: 0.5, in: .meters)
WindowGroup(id: "viewer") { VolumeView() }
.windowStyle(.volumetric)
Entity Interaction
entity.components.set(CollisionComponent(shapes: [.generateSphere(radius: 0.1)]))
entity.components.set(InputTargetComponent())
entity.components.set(InputTargetComponent())
Immersive Space Lifecycle
let result = await openImmersiveSpace(id: "mySpace")
switch result {
case .opened: isImmersed = true
case .userCancelled, .error: showError = true
@unknown default: break
}
await openImmersiveSpace(id: "mySpace")
Loading Models
RealityView { content in
do {
let model = try await ModelEntity(named: "robot")
content.add(model)
} catch {
print("Failed to load model: \(error)")
}
}
let model = try! await ModelEntity(named: "robot")
Gesture Targeting
.gesture(SpatialTapGesture().targetedToAnyEntity().onEnded { value in
value.entity.scale *= 1.2
})
.gesture(TapGesture().onEnded { })
Shared Space Etiquette
.defaultSize(width: 0.5, height: 0.5, depth: 0.5, in: .meters)
.defaultSize(width: 3.0, height: 3.0, depth: 3.0, in: .meters)