visionOS 26
Build spatial computing apps for Apple Vision Pro
🎯 What Makes visionOS Different
- 3D Space: Apps exist in physical space
- Spatial Input: Eyes, hands, voice
- Immersion: From windows to full immersion
- Depth: Real depth perception
🚀 Your First visionOS App (10 min)
import SwiftUI
import RealityKit
@main
struct HelloVisionApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
VStack(spacing: 30) {
Text("Hello, Vision Pro!")
.font(.extraLargeTitle)
Model3D(named: "Scene") { model in
model
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
}
.frame(depth: 300)
}
.padding()
}
}
New: .frame(depth:) adds 3D depth!
🎨 Windows, Volumes, and Spaces
1. Window (2D Content)
WindowGroup {
ContentView()
}
Use for: Settings, lists, forms
2. Volume (3D Content)
WindowGroup(id: "model") {
Model3DView()
}
.windowStyle(.volumetric)
.defaultSize(width: 0.5, height: 0.5, depth: 0.5, in: .meters)
Use for: 3D models, games, visualizations
3. Immersive Space (Full Immersion)
ImmersiveSpace(id: "immersive") {
ImmersiveView()
}
.immersionStyle(selection: .constant(.full), in: .full)
Use for: Games, experiences, meditation apps
🎯 Complete Example: 3D Gallery
import SwiftUI
import RealityKit
@main
struct GalleryApp: App {
var body: some Scene {
WindowGroup {
GalleryView()
}
ImmersiveSpace(id: "gallery") {
ImmersiveGalleryView()
}
}
}
struct GalleryView: View {
@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
@State private var isImmersive = false
var body: some View {
VStack(spacing: 20) {
Text("3D Art Gallery")
.font(.extraLargeTitle)
Button(isImmersive ? "Exit Gallery" : "Enter Gallery") {
Task {
if isImmersive {
await dismissImmersiveSpace()
} else {
await openImmersiveSpace(id: "gallery")
}
isImmersive.toggle()
}
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
struct ImmersiveGalleryView: View {
var body: some View {
RealityView { content in
// Create 3D scene
let artwork1 = createArtwork(at: SIMD3(x: -1, y: 1.5, z: -2))
let artwork2 = createArtwork(at: SIMD3(x: 0, y: 1.5, z: -2))
let artwork3 = createArtwork(at: SIMD3(x: 1, y: 1.5, z: -2))
content.add(artwork1)
content.add(artwork2)
content.add(artwork3)
}
}
private func createArtwork(at position: SIMD3<Float>) -> Entity {
let mesh = MeshResource.generateBox(width: 0.5, height: 0.7, depth: 0.05)
let material = SimpleMaterial(color: .blue, isMetallic: false)
let entity = ModelEntity(mesh: mesh, materials: [material])
entity.position = position
return entity
}
}
👁️ Spatial Input
Eye Tracking
struct InteractiveView: View {
@State private var isLookedAt = false
var body: some View {
RealityView { content in
let entity = ModelEntity(mesh: .generateSphere(radius: 0.1))
entity.components.set(InputTargetComponent())
entity.components.set(HoverEffectComponent())
content.add(entity)
}
.onContinuousHover { phase in
switch phase {
case .active:
isLookedAt = true
case .ended:
isLookedAt = false
}
}
}
}
Hand Gestures
struct GestureView: View {
@State private var scale: Float = 1.0
var body: some View {
RealityView { content in
let entity = ModelEntity(mesh: .generateBox(size: 0.2))
entity.components.set(InputTargetComponent())
content.add(entity)
}
.gesture(
MagnifyGesture()
.onChanged { value in
scale = Float(value.magnification)
}
)
}
}
🎮 RealityKit Basics
Create 3D Objects
// Sphere
let sphere = ModelEntity(
mesh: .generateSphere(radius: 0.1),
materials: [SimpleMaterial(color: .red, isMetallic: true)]
)
// Box
let box = ModelEntity(
mesh: .generateBox(size: 0.2),
materials: [SimpleMaterial(color: .blue, isMetallic: false)]
)
// Custom mesh
let mesh = MeshResource.generateBox(width: 0.3, height: 0.2, depth: 0.1)
let entity = ModelEntity(mesh: mesh)
Positioning
entity.position = SIMD3(x: 0, y: 1.5, z: -2)
entity.orientation = simd_quatf(angle: .pi / 4, axis: [0, 1, 0])
entity.scale = SIMD3(repeating: 1.5)
Animation
var transform = entity.transform
transform.translation.y += 0.5
entity.move(
to: transform,
relativeTo: nil,
duration: 1.0,
timingFunction: .easeInOut
)
🌍 Spatial Anchors
Place Objects in Real World
import ARKit
struct AnchoredView: View {
var body: some View {
RealityView { content in
// Create anchor
let anchor = AnchorEntity(.plane(.horizontal, classification: .floor, minimumBounds: [0.5, 0.5]))
// Add object to anchor
let entity = ModelEntity(mesh: .generateBox(size: 0.2))
anchor.addChild(entity)
content.add(anchor)
}
}
}
🎯 Practical Example: Solar System
struct SolarSystemView: View {
var body: some View {
RealityView { content in
// Sun
let sun = createPlanet(radius: 0.3, color: .yellow)
sun.position = [0, 1.5, -2]
content.add(sun)
// Earth
let earth = createPlanet(radius: 0.1, color: .blue)
earth.position = [0.8, 1.5, -2]
content.add(earth)
// Orbit animation
animateOrbit(earth, around: sun)
}
}
private func createPlanet(radius: Float, color: UIColor) -> ModelEntity {
let mesh = MeshResource.generateSphere(radius: radius)
let material = SimpleMaterial(color: color, isMetallic: false)
return ModelEntity(mesh: mesh, materials: [material])
}
private func animateOrbit(_ planet: ModelEntity, around center: ModelEntity) {
// Circular orbit animation
let duration: TimeInterval = 10.0
Timer.scheduledTimer(withTimeInterval: 0.016, repeats: true) { _ in
let angle = Float(Date().timeIntervalSince1970.truncatingRemainder(dividingBy: duration) / duration * 2 * .pi)
planet.position.x = center.position.x + 0.8 * cos(angle)
planet.position.z = center.position.z + 0.8 * sin(angle)
}
}
}
🎨 Materials and Lighting
Physical Materials
var material = PhysicallyBasedMaterial()
material.baseColor = .init(tint: .blue)
material.roughness = 0.3
material.metallic = 0.8
let entity = ModelEntity(mesh: mesh, materials: [material])
Image-Based Lighting
// Add environment lighting
let environment = try await EnvironmentResource(named: "studio")
entity.components.set(ImageBasedLightComponent(source: .single(environment)))
🎯 Passthrough and Immersion
@main
struct ImmersiveApp: App {
@State private var immersionLevel: ImmersionStyle = .mixed
var body: some Scene {
ImmersiveSpace(id: "space") {
ContentView()
}
.immersionStyle(selection: $immersionLevel, in: .mixed, .progressive, .full)
}
}
Levels:
.mixed: See real world + virtual objects.progressive: Gradually fade real world.full: Complete virtual environment
🎮 Game Example: Catch the Balls
struct CatchGameView: View {
@State private var score = 0
var body: some View {
RealityView { content in
// Spawn balls
Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { _ in
let ball = createBall()
content.add(ball)
animateFall(ball)
}
} update: { content in
// Update score display
}
.overlay(alignment: .top) {
Text("Score: \(score)")
.font(.extraLargeTitle)
.padding()
}
}
private func createBall() -> ModelEntity {
let ball = ModelEntity(
mesh: .generateSphere(radius: 0.1),
materials: [SimpleMaterial(color: .red, isMetallic: false)]
)
ball.position = SIMD3(
x: Float.random(in: -1...1),
y: 2,
z: -2
)
ball.components.set(InputTargetComponent())
return ball
}
private func animateFall(_ ball: ModelEntity) {
var transform = ball.transform
transform.translation.y = 0
ball.move(to: transform, relativeTo: nil, duration: 3.0)
}
}
💡 Best Practices
1. Comfortable Viewing Distance
// Place content 1-3 meters away
entity.position.z = -2.0 // 2 meters
2. Appropriate Scale
// Real-world scale
let chair = ModelEntity(mesh: chairMesh)
chair.scale = SIMD3(repeating: 1.0) // 1:1 scale
3. Performance
// Use LOD (Level of Detail)
entity.components.set(ModelComponent(
mesh: mesh,
materials: materials
))
// Limit polygon count
// Target: < 100K polygons per scene
4. Accessibility
// Add accessibility labels
entity.accessibilityLabel = "Red sphere"
entity.accessibilityHint = "Tap to interact"
🎯 Testing
Simulator
# Run in visionOS Simulator
xcodebuild -scheme YourApp \
-destination 'platform=visionOS Simulator,name=Apple Vision Pro'
Device
- Requires Apple Vision Pro
- Use Xcode wireless debugging
- Test with real spatial input
📚 Resources
🔗 Next Steps
Remember: Think in 3D space. Design for comfort. Test on device.