Augmented Reality with ARKit and RealityKit
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Every note in FieldNotes can carry a NoteLocation (added on Persisting Models with
SwiftData and filled in on Location and Maps). A flat map shows where
notes were taken; this page shows them in space. On iPhone and iPad, FieldNotesARView stands a small pin
for each nearby note on a real table, seen through the camera. On Apple Vision Pro, NotesMapSpace, the
immersive space that part 1 declared as ImmersiveSpace(id: "map"), lays a 3D map of the field site on the
person’s desk with the pins standing on it. The page uses the 27 releases (iOS, iPadOS, macOS, watchOS and
visionOS 27), Xcode 27 and Swift 6.4, with the app’s deployment minimum of 26; the few RealityKit
features that are new in 27 are guarded with if #available.
Two frameworks share the work. RealityKit renders and simulates 3D content and is the same on every platform
that has it. ARKit understands the real world, such as the device’s position, planes, meshes of the room and
hands, and has a different API on iOS and on visionOS. Both are shared by the two AR views, which reuse one
component, NotePinComponent, and one system, NotePinSystem.
Two Frameworks, Five Platforms
Platform availability
| API or feature | iOS | iPadOS | macOS | watchOS | visionOS |
|---|---|---|---|---|---|
RealityKit |
Yes |
Yes |
Yes |
Not available |
Yes |
|
Yes (camera content) |
Yes (camera content) |
Yes (virtual camera only) |
Not available |
Yes ( |
|
Yes |
Yes |
Not available |
Not available |
|
ARKit |
Yes (A12 chip or later) |
Yes |
Not available |
Not available |
Not available (use |
ARKit |
Not available |
Not available |
|
Not available |
Yes, in a Full Space |
Reality Composer Pro 3 assets ( |
Yes |
Yes |
Yes |
Not available |
Yes |
|
New in 27 |
New in 27 |
New in 27 |
Not available |
New in 27 |
|
New in 27 |
New in 27 |
Splats only, new in 27 |
Not available |
New in 27 |
RealityKit also runs on tvOS since tvOS 26, where RealityView renders with a virtual camera like on the Mac;
FieldNotes does not ship a tvOS app. The watch app has no 3D view, so every file on this page is wrapped in
#if canImport(RealityKit) or a narrower #if os(…) so the shared app target still builds for watchOS.
RealityKit’s Entity-Component-System Model
RealityKit organizes a scene with the entity-component-system (ECS) pattern that game engines use:
-
an entity (
Entity) is only an identity with a place in a hierarchy: a parent, children and a transform; -
a component (a type conforming to
Component) is plain data attached to an entity: a mesh and materials (ModelComponent), a collision shape (CollisionComponent), the fact that it can be tapped (InputTargetComponent), or app-defined data such asNotePinComponent; -
a system (a type conforming to
System) is behaviour: every frame it queries the scene for the entities that have certain components and updates them.
An entity has at most one component of each type. Behaviour is added by adding components, not by subclassing,
so a note pin, a terrain tile and a fingertip marker are all plain Entity or ModelEntity instances with
different components. Reality Composer Pro is built on the same model, so components defined in code can also be
attached to entities in the editor.
Figure: Entity, Components and Systems in FieldNotes
NotePinComponent
The component holds everything the system needs to animate a pin, and nothing else. It is Codable so that
Reality Composer Pro can show it in its inspector and save it in a scene (see Reality Composer Pro Assets).
// FieldNotes/AR/NotePinComponent.swift
#if canImport(RealityKit)
import FieldNotesModel
import RealityKit
struct NotePinComponent: Component, Codable {
var noteID: Note.ID
var isPinned: Bool
var restHeight: Float = 0.06 // metres above the surface
var bobPhase: Float = 0 // advanced by NotePinSystem
}
#endif
NotePinSystem
The system runs once per frame for the whole scene. It asks the update context for every entity with a
NotePinComponent, makes each pin bob gently (pinned notes faster, so they stand out), and writes the component
back. Components are value types, so the change is only stored when components.set(_:) is called.
// FieldNotes/AR/NotePinSystem.swift
#if canImport(RealityKit)
import Foundation
import RealityKit
struct NotePinSystem: System {
private static let query = EntityQuery(where: .has(NotePinComponent.self))
init(scene: RealityKit.Scene) {}
func update(context: SceneUpdateContext) {
let deltaTime = Float(context.deltaTime)
for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
guard var pin = entity.components[NotePinComponent.self] else { continue }
pin.bobPhase += deltaTime * (pin.isPinned ? 4 : 2)
entity.position.y = pin.restHeight + 0.008 * sin(pin.bobPhase)
entity.components.set(pin)
}
}
}
#endif
See Apple Developer Documentation — System, Apple Developer Documentation — EntityQuery and Apple Developer Documentation — entities(matching:updatingSystemWhen:).
RealityKit.Scene is spelled out because SwiftUI also has a Scene protocol. .rendering makes the system run
only while the scene is actually drawing, which saves battery when the view is hidden. Systems are created and
called by RealityKit on the main actor.
Registering them once
Custom components and systems must be registered before any scene uses them. FieldNotesApp does it in its
initializer, next to the part 1 scene declarations:
// FieldNotes/FieldNotesApp.swift (excerpt)
init() {
#if canImport(RealityKit)
NotePinComponent.registerComponent()
NotePinSystem.registerSystem()
#endif
}
Building a pin entity
A small factory, shared by every platform, turns a Note into an entity: a sphere on a thin stick, a floating
title that always faces the viewer, and the components that make it tappable and animated.
// FieldNotes/AR/NotePinFactory.swift
#if canImport(RealityKit)
import FieldNotesModel
import RealityKit
#if canImport(UIKit)
import UIKit // UIColor on iOS and visionOS
#else
import AppKit // NSColor on macOS; both have .systemOrange, .systemBlue and .gray
#endif
enum NotePinFactory {
@MainActor
static func makePin(for note: Note, at position: SIMD3<Float>) -> Entity {
let pin = Entity()
pin.name = note.id.uuidString
pin.position = position
let head = ModelEntity(mesh: .generateSphere(radius: 0.012),
materials: [SimpleMaterial(color: note.isPinned ? .systemOrange : .systemBlue,
isMetallic: false)])
head.position.y = 0.03
let stick = ModelEntity(mesh: .generateCylinder(height: 0.06, radius: 0.0015),
materials: [SimpleMaterial(color: .gray, isMetallic: true)])
pin.addChild(stick)
pin.addChild(head)
let label = Entity()
var text = TextComponent()
text.text = AttributedString(note.title)
text.size = CGSize(width: 240, height: 60)
label.components.set(text)
label.components.set(BillboardComponent()) // always faces the camera
label.position.y = 0.06
pin.addChild(label)
pin.components.set(NotePinComponent(noteID: note.id, isPinned: note.isPinned))
pin.components.set(InputTargetComponent())
pin.components.set(CollisionComponent(shapes: [.generateSphere(radius: 0.03)]))
return pin
}
}
#endif
See Apple Developer Documentation — ModelEntity, Apple Developer Documentation — TextComponent, Apple Developer Documentation — BillboardComponent and Apple Developer Documentation — InputTargetComponent.
InputTargetComponent and CollisionComponent are both needed for a SwiftUI gesture to hit the entity: the first
opts it into input, the second gives input a shape to hit. SimpleMaterial takes a UIColor on iOS and
visionOS and an NSColor on macOS, which is why the imports are conditional.
A small layout helper converts a note’s latitude and longitude into metres on the table, relative to the first note, with an equirectangular projection that is accurate enough over a field site of a few kilometres:
// FieldNotes/AR/NotesMapLayout.swift
import FieldNotesModel
import Foundation
enum NotesMapLayout {
/// Positions in metres, scaled so `span` metres of terrain fit in `tableSize` metres.
static func positions(for notes: [Note], tableSize: Float = 0.6, span: Double = 2_000)
-> [(note: Note, position: SIMD3<Float>)] {
let located = notes.filter { $0.location != nil }
guard let origin = located.first?.location else { return [] }
let metresPerDegree = 111_320.0
let scale = Double(tableSize) / span
return located.map { note in
let loc = note.location!
let east = (loc.longitude - origin.longitude) * metresPerDegree
* cos(origin.latitude * .pi / 180)
let north = (loc.latitude - origin.latitude) * metresPerDegree
return (note, SIMD3(Float(east * scale), 0, Float(-north * scale))) // -z is north
}
}
}
NotePinFactory and NotesMapLayout are page-local helpers of the app target.
RealityView Across Platforms
RealityView is the SwiftUI view that hosts RealityKit content. Its make closure runs once, asynchronously,
to build the scene; its optional update closure runs whenever SwiftUI state the closure reads changes. The
content type differs by platform:
-
on iOS, iPadOS, macOS and tvOS, the closures receive a
RealityViewCameraContent, which has acamera(a virtual camera, or on iOS the device camera with world tracking); -
on visionOS, they receive a
RealityViewContent: the content is placed in the person’s space, and there is no camera to set, because the person’s eyes are the camera.
On the Mac, FieldNotes shows the same map in an ordinary window, with orbit controls instead of AR. It is a compact example of the shared code:
// FieldNotes/AR/NotesMapPreview.swift (macOS window content)
#if os(macOS)
import FieldNotesServices
import RealityKit
import SwiftUI
struct NotesMapPreview: View {
@Environment(NoteStore.self) private var store
var body: some View {
RealityView { content in
content.camera = .virtual
let root = Entity()
for (note, position) in NotesMapLayout.positions(for: store.notes) {
root.addChild(NotePinFactory.makePin(for: note, at: position))
}
content.add(root)
}
.realityViewCameraControls(.orbit)
}
}
#Preview {
NotesMapPreview()
.environment(NoteStore(notes: [/* sample notes with locations */]))
}
#endif
See Apple Developer Documentation — RealityView, Apple Developer Documentation — RealityViewCameraContent and Apple Developer Documentation -- realityViewCameraControls(_:).
NotesMapPreview is a page-local view; the Mac app opens it in a Window scene, which passes the store with
.environment(store) like every other FieldNotes scene.
ARKit on iOS: FieldNotesARView
On iPhone and iPad, AR means drawing virtual content over the camera feed, locked to the real world as the device moves. ARKit’s world tracking fuses the camera image with the motion sensors to estimate the device’s position and orientation in metres, 60 times a second. On top of it, plane detection finds horizontal and vertical surfaces, and anchors pin content to a real place so it stays put as ARKit’s understanding of the room improves.
Since iOS 18, the simplest way to get all of that is from RealityView itself: set content.camera =
.spatialTracking, and anchor content with an AnchorEntity whose target is a plane. RealityKit runs the ARKit
session for you. SpatialTrackingSession then gives the app access to the anchors' real transforms, which
FieldNotes needs to know when a table has been found:
// FieldNotes/AR/FieldNotesARView.swift
#if os(iOS)
import ARKit
import FieldNotesModel
import FieldNotesServices
import RealityKit
import SwiftUI
struct FieldNotesARView: View {
@Environment(NoteStore.self) private var store
@State private var trackingSession = SpatialTrackingSession()
@State private var selectedNote: Note?
@State private var trackingProblem: String?
var body: some View {
RealityView { content in
content.camera = .spatialTracking
// Wait for a real horizontal table at least 30 cm x 30 cm, then place the pins on it.
let table = AnchorEntity(.plane(.horizontal, classification: .table,
minimumBounds: [0.3, 0.3]),
trackingMode: .once)
for (note, position) in NotesMapLayout.positions(for: store.notes, tableSize: 0.3) {
table.addChild(NotePinFactory.makePin(for: note, at: position))
}
content.add(table)
}
.gesture(
SpatialTapGesture()
.targetedToAnyEntity()
.onEnded { value in
guard let pin = value.entity.components[NotePinComponent.self] else { return }
selectedNote = store.notes.first { $0.id == pin.noteID }
}
)
.sheet(item: $selectedNote) { note in
NoteDetail(note: note)
}
.overlay(alignment: .top) {
if let trackingProblem { Text(trackingProblem).padding().background(.regularMaterial) }
}
.task {
guard ARWorldTrackingConfiguration.isSupported else {
trackingProblem = "This device doesn't support world tracking."
return
}
let configuration = SpatialTrackingSession.Configuration(tracking: [.plane, .camera])
if let unavailable = await trackingSession.run(configuration) {
trackingProblem = "Some tracking is unavailable: \(unavailable)"
}
}
}
}
#endif
// ContentView (iOS excerpt): presenting it, with the store injected explicitly.
// .fullScreenCover(isPresented: $showsARPins) {
// FieldNotesARView().environment(store)
// }
See Apple Developer Documentation — spatialTracking, Apple Developer Documentation — AnchorEntity, Apple Developer Documentation — SpatialTrackingSession and Apple Developer Documentation — targetedToAnyEntity().
Needs the NSCameraUsageDescription key in Info.plist and a physical iPhone or iPad with an A12 chip or later;
the simulator has no camera and cannot run world tracking. Plane classification (.table) also needs a device
that supports it; on others, use .any.
trackingMode: .once places the pins when the table is first found and then leaves them fixed in the world;
.continuous would make them follow every refinement of the plane, which looks jittery for small content.
Note is Identifiable, which is what .sheet(item:) needs; NoteDetail(note:) is part 1’s detail view.
Working with ARKit directly
SpatialTrackingSession hides ARKit’s own types. When the app needs them, for example to read plane
classifications, add its own ARAnchors or turn on scene reconstruction on LiDAR devices, it can run the same
session with an explicit ARSession and ARWorldTrackingConfiguration, and observe the session through its
delegate:
// FieldNotes/AR/TableFinder.swift
#if os(iOS)
import ARKit
import Observation
import RealityKit
@Observable @MainActor
final class TableFinder: NSObject, ARSessionDelegate {
private(set) var foundTable = false
let arSession = ARSession()
func start(with trackingSession: SpatialTrackingSession) async {
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal, .vertical]
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) {
configuration.sceneReconstruction = .mesh // LiDAR devices only
}
arSession.delegate = self
_ = await trackingSession.run(.init(tracking: [.plane, .camera]),
session: arSession, arConfiguration: configuration)
}
/// Remembers where a note was placed, so ARKit keeps refining that exact spot.
func addAnchor(for noteID: UUID, at transform: simd_float4x4) {
arSession.add(anchor: ARAnchor(name: "note-\(noteID.uuidString)", transform: transform))
}
nonisolated func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
let isTable = anchors.contains { ($0 as? ARPlaneAnchor)?.classification == .table }
guard isTable else { return }
Task { @MainActor in self.foundTable = true }
}
}
#endif
See Apple Developer Documentation -- run(_:session:arConfiguration:), Apple Developer Documentation — ARKit, Apple Developer Documentation — ARWorldTrackingConfiguration, Apple Developer Documentation — planeDetection, Apple Developer Documentation — ARPlaneAnchor, Apple Developer Documentation — ARAnchor and Apple Developer Documentation — Tracking and visualizing planes.
The delegate callback is nonisolated because ARKit calls it on its own queue; it extracts a Bool and hops to
the main actor, so no ARKit object crosses actors. The pattern is the one described on
Actors, Isolation and Sendable.
TableFinder is a page-local helper. The iOS-only camera, media and ARKit capture APIs outside AR views are on
Camera, Media and Photos.
ARKit on visionOS: NotesMapSpace
On Apple Vision Pro there is no camera image for apps and no ARSession. Instead, an ARKitSession runs
data providers, each of which delivers one kind of information as an async sequence of anchor updates:
| Provider | Delivers | Authorization |
|---|---|---|
|
The device pose and app-placed |
None |
|
|
World sensing ( |
|
|
World sensing |
|
|
Hand tracking ( |
To protect privacy, ARKit delivers this data only while the app is in a Full Space, which means an
ImmersiveSpace is open and other apps are hidden. FieldNotes’s ImmersiveSpace(id: "map") uses the default
mixed immersion style, so the person still sees their room, and that already counts as a Full Space. Opening the
space is shown on App Structure and Life Cycle.
NotesMapSpace reads the note store, so the scene must inject it. Part 1’s declaration gains one modifier:
// FieldNotes/FieldNotesApp.swift (visionOS excerpt, updated)
#if os(visionOS)
ImmersiveSpace(id: "map") {
NotesMapSpace()
.environment(store) // NotesMapSpace reads @Environment(NoteStore.self)
}
#endif
A page-local tracker owns the session and its providers and turns their updates into entities:
// FieldNotes/AR/NotesMapTracking.swift
#if os(visionOS)
import ARKit
import Observation
import RealityKit
@Observable @MainActor
final class NotesMapTracking {
private(set) var tableTransform: simd_float4x4?
let meshRoot = Entity() // collision meshes of the room
let fingertip = ModelEntity(mesh: .generateSphere(radius: 0.006),
materials: [UnlitMaterial(color: .white)])
private let session = ARKitSession()
private let planes = PlaneDetectionProvider(alignments: [.horizontal])
private let meshes = SceneReconstructionProvider()
private let hands = HandTrackingProvider()
private var meshEntities: [UUID: Entity] = [:]
func run() async {
var providers: [any DataProvider] = []
if PlaneDetectionProvider.isSupported { providers.append(planes) }
if SceneReconstructionProvider.isSupported { providers.append(meshes) }
if HandTrackingProvider.isSupported { providers.append(hands) }
do {
try await session.run(providers) // shows the permission prompts if needed
} catch {
return
}
await withTaskGroup(of: Void.self) { group in
group.addTask { await self.processPlanes() }
group.addTask { await self.processMeshes() }
group.addTask { await self.processHands() }
}
}
private func processPlanes() async {
for await update in planes.anchorUpdates where update.anchor.classification == .table {
if update.event != .removed, tableTransform == nil {
tableTransform = update.anchor.originFromAnchorTransform // first table wins
}
}
}
private func processMeshes() async {
for await update in meshes.anchorUpdates {
let anchor = update.anchor
switch update.event {
case .added, .updated:
guard let shape = try? await ShapeResource.generateStaticMesh(from: anchor) else { continue }
let entity = meshEntities[anchor.id] ?? Entity()
entity.components.set(CollisionComponent(shapes: [shape], isStatic: true))
entity.setTransformMatrix(anchor.originFromAnchorTransform, relativeTo: nil)
if meshEntities[anchor.id] == nil {
meshEntities[anchor.id] = entity
meshRoot.addChild(entity)
}
case .removed:
meshEntities.removeValue(forKey: anchor.id)?.removeFromParent()
}
}
}
private func processHands() async {
for await update in hands.anchorUpdates {
let hand = update.anchor
guard hand.isTracked, hand.chirality == .right,
let tip = hand.handSkeleton?.joint(.indexFingerTip), tip.isTracked else { continue }
fingertip.setTransformMatrix(hand.originFromAnchorTransform * tip.anchorFromJointTransform,
relativeTo: nil)
}
}
}
#endif
See Apple Developer Documentation — ARKitSession, Apple Developer Documentation — PlaneDetectionProvider, Apple Developer Documentation — SceneReconstructionProvider and Apple Developer Documentation — HandTrackingProvider.
Each provider’s loop is an infinite async sequence, so the three run in parallel in a task group; the class is main-actor isolated, so each loop hops back to the main actor to touch entities. The scene-reconstruction meshes give the room collision shapes, so a pin dropped off the edge of the table falls onto the real floor instead of through it; the fingertip marker makes it easy to point at a small pin.
NotesMapSpace itself replaces part 1’s placeholder body (a green plane) with the real map. The type and the
scene declaration stay the same:
// FieldNotes/Views/NotesMapSpace.swift
#if os(visionOS)
import FieldNotesModel
import FieldNotesServices
import RealityKit
import RealityKitContent
import SwiftUI
struct NotesMapSpace: View {
@Environment(NoteStore.self) private var store
@State private var tracking = NotesMapTracking()
@State private var selectedNote: Note?
var body: some View {
RealityView { content in
let map = Entity()
map.name = "NotesMap"
if let terrain = try? await Entity(named: "FieldSiteTerrain", in: realityKitContentBundle) {
map.addChild(terrain) // authored in Reality Composer Pro
}
for (note, position) in NotesMapLayout.positions(for: store.notes) {
map.addChild(NotePinFactory.makePin(for: note, at: position))
}
map.isEnabled = false // hidden until a table is found
content.add(map)
content.add(tracking.meshRoot)
content.add(tracking.fingertip)
} update: { content in
guard let table = tracking.tableTransform,
let map = content.entities.first(where: { $0.name == "NotesMap" }) else { return }
map.transform = Transform(matrix: table)
map.isEnabled = true
}
.gesture(
SpatialTapGesture()
.targetedToAnyEntity()
.onEnded { value in
guard let pin = value.entity.components[NotePinComponent.self] else { return }
selectedNote = store.notes.first { $0.id == pin.noteID } // shown in a window, see below
}
)
.task { await tracking.run() }
}
}
#endif
See Apple Developer Documentation — RealityViewContent, Apple Developer Documentation — Setting up access to ARKit data and Apple Developer Documentation — Adding 3D content to your app.
Needs the NSWorldSensingUsageDescription and NSHandsTrackingUsageDescription keys in Info.plist and a
physical Apple Vision Pro; the visionOS simulator supports none of the three providers, so isSupported is
false there and the map simply stays hidden. How the selected note is shown next to the map, as a window or
an attachment, belongs to the windows-volumes-and-spaces discussion on
visionOS: Windows, Volumes, Immersive Spaces and RealityKit.
update runs whenever tracking.tableTransform changes, because the closure reads it and NotesMapTracking is
@Observable. That is the idiomatic way to move RealityKit content in response to app state: never keep
references to entities in SwiftUI state, and never mutate the scene from outside make, update, a system or
a gesture handler.
To make the map reappear in the same place next time, the app can add a WorldAnchor at the table’s transform
with WorldTrackingProvider.addAnchor(_:); world anchors persist across launches on the same device.
let worldTracking = WorldTrackingProvider()
// after session.run([worldTracking, ...]):
try await worldTracking.addAnchor(WorldAnchor(originFromAnchorTransform: table))
Reality Composer Pro Assets
Reality Composer Pro is Apple’s 3D content tool for RealityKit. Version 3, released alongside the 27
releases, is a separate download that needs macOS Tahoe 26.5 or later. It edits scenes as USD (.usda) files,
designs materials with Shader Graph, and in version 3 adds visual scripting (Script Graph), GPU simulations
(Compute Graph), animation graphs and a lightmap baker. A visionOS app template in Xcode already includes a
Swift package, conventionally named RealityKitContent, whose .rkassets folder holds the Reality Composer
Pro project; FieldNotes keeps it in Packages/RealityKitContent and uses it on iOS, macOS and visionOS.
Loading a scene from the package is one call. The package generates a realityKitContentBundle constant for its
resource bundle:
import RealityKit
import RealityKitContent
let terrain = try await Entity(named: "FieldSiteTerrain", in: realityKitContentBundle)
See Apple Developer Documentation — init(named:in:) and Apple Developer Documentation — Reality Composer Pro.
Because NotePinComponent is Codable and registered, a component with the same name defined inside the
RealityKitContent package appears in Reality Composer Pro’s Add Component list, so a designer can place
hand-made pins in the terrain scene and the same NotePinSystem animates them. For that, move the component’s
declaration into the package and make it public.
What RealityKit Adds in 27
The WWDC26 RealityKit session introduces several features, all new in 27 on iOS, iPadOS, macOS and visionOS (some on tvOS too):
-
lighting and shadows: soft shadows for dynamic lights (
SpotLightComponent.Shadow.lightSize), projective textures, and lights that affect the real surroundings; -
navigation mesh:
NavigationControllerfinds paths across a navigation mesh for characters; -
cloth simulation:
ClothBodyComponentfor curtains, flags and clothing; -
level of detail:
LevelOfDetailComponentswaps meshes by camera distance or screen area; -
3D Gaussian splats:
GaussianSplatComponentrenders captured real-world scenes; -
immersive audio: reverb meshes that model a room’s acoustics on Apple Vision Pro.
FieldNotes adopts level of detail for the terrain, because a full-resolution field site is expensive when the map is small in the view. The deployment minimum is 26, so the call is guarded:
// NotesMapSpace, inside make: three versions of the terrain from Reality Composer Pro
if #available(visionOS 27, iOS 27, macOS 27, *),
let high = try? await Entity(named: "FieldSiteTerrain", in: realityKitContentBundle),
let medium = try? await Entity(named: "FieldSiteTerrainMedium", in: realityKitContentBundle),
let low = try? await Entity(named: "FieldSiteTerrainLow", in: realityKitContentBundle) {
let terrain = Entity()
LevelOfDetailComponent.addByCameraDistance(to: terrain, levels: [
(entities: [high], maxDistance: 1.0), // metres
(entities: [medium], maxDistance: 3.0),
(entities: [low], maxDistance: .infinity),
])
map.addChild(terrain)
}
Where to Go Next
This page covers what FieldNotes draws and how it tracks the world. The visionOS app model around it, including windows, volumes, ornaments, immersion styles, shared spaces and spatial design, is on visionOS: Windows, Volumes, Immersive Spaces and RealityKit. Performance profiling of RealityKit scenes with Instruments is on Performance and Instruments. The immersive-experience design guidance is in Human Interface Guidelines — Immersive experiences.
References
-
Apple Developer Documentation — entities(matching:updatingSystemWhen:)
-
Apple Developer Documentation -- realityViewCameraControls(_:)
-
Apple Developer Documentation -- run(_:session:arConfiguration:)
-
Apple Developer Documentation — ARWorldTrackingConfiguration
-
Apple Developer Documentation — Tracking and visualizing planes
-
Apple Developer Documentation — Setting up access to ARKit data
-
Apple Developer Documentation — Adding 3D content to your app