If you search for "ARKit tutorial" in 2026 you will still find pages of 2017 articles that open with ARSCNView and a SCNScene. Do not follow them. Apple deprecated SceneKit at WWDC 2025 and moved it to critical-bug-only maintenance, and the modern path — RealityView in SwiftUI, backed by RealityKit and ARKit — is both simpler and the only one that carries over to Apple Vision Pro.
This tutorial builds a small but complete AR app: it detects a horizontal surface, anchors a USDZ model to it, and lets the user drag and rotate the model. Everything here targets iOS 18 or later with Xcode 16 or later (Xcode 26 is current); nothing depends on beta APIs.
1. Project setup
Create a new iOS App project in Xcode with SwiftUI as the interface. Then:
- Add the camera usage description. In the target's Info tab add
NSCameraUsageDescriptionwith a sentence such as "This app uses the camera to place 3D content in your space." - Add a model. Any USDZ works; Apple's AR Quick Look Gallery has free samples. Drag
toy_biplane.usdz(or your own file) into the project and make sure it is added to the app target. - You do not need to import an ARKit view controller, a delegate, or a storyboard. RealityKit owns the session on iOS when you use
RealityViewwith the.camera(.spatialTracking)mode.
That is the entire setup. No ARSCNViewDelegate, no SCNNode tree.
2. RealityView in SwiftUI
RealityView is the SwiftUI view that hosts RealityKit content on iOS 18, iPadOS 18, macOS 15, and visionOS. On iOS it renders on top of the camera feed when you ask for spatial tracking.
import SwiftUI
import RealityKit
struct ContentView: View {
var body: some View {
RealityView { content in
// Runs once. Build the scene here.
content.camera = .spatialTracking
}
.ignoresSafeArea()
}
}
Run this on a device (the simulator cannot drive the camera) and you have a live AR viewport. The make closure receives a RealityViewContent; anything you add to it becomes part of the RealityKit scene.
3. Anchoring a USDZ model to a detected plane
RealityKit's AnchorEntity describes where content belongs and lets ARKit do the detection. For a tabletop experience we want a horizontal plane at least a few centimeters across:
import SwiftUI
import RealityKit
struct ContentView: View {
var body: some View {
RealityView { content in
content.camera = .spatialTracking
let anchor = AnchorEntity(
.plane(.horizontal,
classification: .any,
minimumBounds: SIMD2<Float>(0.2, 0.2))
)
do {
let model = try await Entity(named: "toy_biplane")
model.scale = SIMD3<Float>(repeating: 0.01) // USDZ units are usually cm
anchor.addChild(model)
} catch {
print("Failed to load model: \(error)")
}
content.add(anchor)
}
.ignoresSafeArea()
}
}
Two things to notice. Entity(named:) is async and loads from the app bundle, so the make closure is where asynchronous loading belongs. And the anchor's content stays invisible until ARKit actually finds a plane that satisfies the request — you do not write any "plane detected" callback yourself.
If you need to react when the anchor becomes active (to show or hide a coaching hint, for example), subscribe to SceneEvents.AnchoredStateChanged:
content.subscribe(to: SceneEvents.AnchoredStateChanged.self, on: anchor) { event in
print("anchored:", event.isAnchored)
}
Keep the returned subscription alive (store it in a @State variable) or it is cancelled immediately.
4. Gestures: drag and rotate
RealityKit gestures are plain SwiftUI gestures targeted at entities. An entity must have a CollisionComponent and an InputTargetComponent to be hit-testable.
let model = try await Entity(named: "toy_biplane")
model.scale = SIMD3<Float>(repeating: 0.01)
model.generateCollisionShapes(recursive: true)
model.components.set(InputTargetComponent())
anchor.addChild(model)
Now attach the gestures to the RealityView:
struct ContentView: View {
@State private var rotation: Float = 0
var body: some View {
RealityView { content in
// ... scene setup from above ...
}
.gesture(
DragGesture()
.targetedToAnyEntity()
.onChanged { value in
let entity = value.entity
// Convert the drag location into the entity's parent space.
let location = value.convert(value.location3D,
from: .local,
to: entity.parent!)
entity.position = SIMD3<Float>(location.x, entity.position.y, location.z)
}
)
.gesture(
RotateGesture()
.targetedToAnyEntity()
.onChanged { value in
let angle = Float(value.rotation.radians)
value.entity.orientation = simd_quatf(angle: angle, axis: [0, 1, 0])
}
)
.ignoresSafeArea()
}
}
targetedToAnyEntity() turns a standard SwiftUI gesture into an entity-targeted one and gives you value.entity plus 3D coordinate conversion. We clamp the dragged position to the plane's height so the model slides along the surface instead of lifting off it.
5. Updating the scene over time
RealityView has an optional update closure that runs when SwiftUI state the view depends on changes. Use it for reactive changes rather than reaching into the scene from elsewhere:
@State private var showModel = true
RealityView { content in
// build scene
} update: { content in
content.entities.first?.isEnabled = showModel
}
For continuous per-frame logic — a turntable animation, say — add a System or use RealityKit's animation API instead of a timer:
let spin = FromToByAnimation<Transform>(
by: Transform(rotation: simd_quatf(angle: .pi * 2, axis: [0, 1, 0])),
duration: 6,
bindTarget: .transform
)
if let resource = try? AnimationResource.generate(with: spin) {
model.playAnimation(resource.repeat())
}
6. A coaching overlay and a reset button
Users need to know to move the phone to find a surface. On iOS the quickest option is still ARCoachingOverlayView from ARKit, which RealityKit's ARView supports directly. In a SwiftUI-first project, a simple text hint driven by the anchored-state event above is often enough, and it keeps the project free of UIKit. Pair it with a reset button that removes and re-adds the anchor so the user can re-place the model.
7. Why not SceneKit?
You may still see ARSCNView examples that look shorter for the first five minutes. Here is why new projects should not start there:
- Deprecated. Apple's WWDC25 session Bring your SceneKit project to RealityKit is the official guidance: SceneKit gets critical bug fixes only.
- No visionOS. SceneKit does not run on Apple Vision Pro. RealityKit code written against
RealityViewports to a visionOS volume or immersive space with minimal change. - The modern feature set lives in RealityKit. Physically based materials, Reality Composer Pro content, spatial audio, cross-platform ARKit reference objects (iOS 27 / visionOS 27), and
RealityViewattachments are RealityKit features.
If you maintain an existing SceneKit app, read our companion walkthrough, Migrating an ARSCNView App to RealityKit.
Where to go next
- Apple's RealityView documentation and the ARKit documentation.
- Author richer scenes in Reality Composer Pro and load them as a package instead of a single USDZ.
- Need a hand getting an AR project off the ground? RealityRogue's US-based ARKit consultants are available for short engagements and staff augmentation. Get in touch.