Build 3D and AR apps on Apple platforms (iOS, macOS, visionOS) with SceneViewSwift — the SwiftUI wrapper around RealityKit. Use whenever the user asks for "3D in SwiftUI", "AR with ARKit in SwiftUI", a model viewer for iOS, or any Apple-platform 3D/AR app where the dependency is the `SceneViewSwi...
BY sceneviewUPDATED Oct 27, 2026
INTENDED FOR✳Claude Code>_Codex◈OpenClaw↗CursorInstall in a listed agent.
instructions.md
---
name: sceneview-ios
description: Build 3D and AR apps on Apple platforms (iOS, macOS, visionOS) with SceneViewSwift — the SwiftUI wrapper around RealityKit. Use whenever the user asks for "3D in SwiftUI", "AR with ARKit in SwiftUI", a model viewer for iOS, or any Apple-platform 3D/AR app where the dependency is the `SceneViewSwift` Swift Package from `github.com/sceneview/sceneview`. For Jetpack Compose / Android use the `sceneview` skill instead; for the browser use `sceneview-web`. Skip for plain ARKit-SDK / SceneKit / Unity / Unreal / RealityKit-without-SceneViewSwift work.
license: Apache-2.0
metadata:
author: SceneView
source: https://github.com/sceneview/sceneview
last-updated: '2026-10-02'
keywords:
- sceneview
- sceneviewswift
- 3d
- ar
- arkit
- realitykit
- swiftui
- model viewer
- augmented reality
- ios
- macos
- visionos
- swift package manager
- usdz
- stl
- obj
- ply
- mesh formats
- 3d printing
- 3mf
---
## What SceneViewSwift is
SceneViewSwift is the Apple-platform half of the SceneView SDK — a **declarative
SwiftUI** API over **RealityKit**. Same mental model as the Android library
(`SceneView { }` / `ARSceneView { }`) but expressed with SwiftUI result-builders
and view modifiers, not Jetpack Compose.
- **3D** — `SceneView { … }` SwiftUI view (iOS / macOS / visionOS).
- **AR** — `ARSceneView(…)` SwiftUI view (iOS only — ARKit).
- **Renderer** — RealityKit. There is **no Filament** on Apple platforms.
- **Distribution** — Swift Package Manager. The package lives in the
`github.com/sceneview/sceneview` monorepo; consumers add it by URL and pin a
version tag (currently `4.62.0`).
`SceneViewSwift` is consumable directly from Swift, and also underneath the
Flutter and React Native bridges.
## Authoritative API reference
Three sources, in priority order:
1. **`docs/docs/cheatsheet-ios.md`** in the repo — the complete Apple-platform
API reference (every node factory, view modifier, environment preset,
the Android↔Apple mapping table, and the iOS parity-status tables).
<https://github.com/sceneview/sceneview/blob/main/docs/docs/cheatsheet-ios.md>
2. **`SceneViewSwift/Sources/SceneViewSwift/`** — the actual Swift source. When
in doubt about a signature, read it. Node factories live under `Nodes/`,
the views in `SceneView.swift` / `ARSceneView.swift`.
3. **`samples/ios-demo/SceneViewDemo/Views/Demos/`** — a working SwiftUI demo
for every feature. Read the demo, do NOT improvise an API.
`llms.txt` at the repo root carries the cross-platform surface but is
Android-centric — prefer `cheatsheet-ios.md` for Swift.
## When to use this skill
Trigger on any of:
- "Build me a 3D viewer / AR app in SwiftUI."
- "Load a `.usdz` / `.reality` / `.stl` / `.obj` / `.ply` / `.3mf` model on iOS."
- "Open an STL from Files / show a 3D print at real size in AR."
- "Place a model on a detected AR plane in ARKit + SwiftUI."
- "Add 3D content to a visionOS / macOS app with SceneView."
- "Convert this SceneKit / RealityView code to SceneViewSwift."
- "Make `SceneViewer` from our Compose Multiplatform app actually render on iOS."
Skip for plain ARKit-SDK, SceneKit, Unity, Unreal, or RealityKit projects that
do NOT use the SceneViewSwift wrapper.
### If the ask comes from a Compose Multiplatform app
`sceneview-compose` exposes one `SceneViewer` composable from `commonMain`, but a Kotlin
Multiplatform module cannot depend on a Swift Package — so on iOS it renders a "no
renderer registered" notice until the **app** supplies one. That supply is a Swift task,
which is why it lands here:
1. Write an `@objc UIView` wrapping `SceneViewSwift.SceneView` in a
`UIHostingController`. The production-tested pattern is
`flutter/sceneview_flutter/ios/Classes/SceneViewPlugin.swift` — copy its retain-cycle
and Swift-6 actor handling rather than reinventing them.
2. Implement `SceneViewerViewFactory` (`create` **and** `update` — Kotlin/Native does not
bridge interface default bodies into the generated Obj-C protocol, so Swift must
implement both), and assign `SceneViewerBridge.factory` once before the first compose.
3. `update` must **mutate** the existing view. Recreating the scene reloads the model and
throws away the user's camera position.
4. Call the spec's `onCameraMoved` after every gesture — without it `CameraState` reports
only what the app last wrote — and `onError` when a load fails, since a failed load on
RealityKit looks exactly like a slow one.
`SceneViewerSpec` compares by value (model bytes by content), so `update` is **not**
called on every recomposition. Do not build Swift-side change caching that assumes it is.
SceneView does not ship that wrapper yet. If the user wants AR, custom materials or
post-processing, they are outside `sceneview-compose`'s scope — use `ARSceneView` /
`SceneView` from SceneViewSwift directly.
## Setup
```swift
// Package.swift
.package(url: "https://github.com/sceneview/sceneview.git", from: "4.62.0")
```
```swift
import SceneViewSwift
```
For AR, the host app's `Info.plist` must declare `NSCameraUsageDescription`.
For `ARRecorder` saving to Photos, add `NSPhotoLibraryAddUsageDescription`.
## The minimal correct 3D example
Verified against `samples/ios-demo/.../ModelViewerDemo.swift` and the
declarative `@NodeBuilder` initializer in `SceneView.swift`:
```swift
import SwiftUI
import SceneViewSwift
struct ModelViewerScreen: View {
@State private var model: ModelNode?
var body: some View {
SceneView { root in
if let model {
root.addChild(model.entity)
}
}
.environment(.studio) // IBL lighting preset
.cameraControls(.orbit) // .orbit | .pan | .firstPerson | native (iOS 18+): .none | .tilt | .dolly
.autoCenterContent(true)
.task {
model = try? await ModelNode.load("models/helmet.usdz")
model?.scaleToUnits(0.3)
model?.playAllAnimations()
}
}
}
```
The declarative form uses the `@NodeBuilder` result-builder — nodes are
expressions inside the trailing closure:
```swift
SceneView {
GeometryNode.cube(size: 0.3, color: .red)
.position(.init(x: -1, y: 0, z: -2))
GeometryNode.sphere(radius: 0.2, color: .blue)
.position(.init(x: 1, y: 0, z: -2))
}
.environment(.studio)
.cameraControls(.orbit)
```
## Model formats and units
`ModelNode.load(contentsOf:unit:)` is the **one entry point for every supported
format**. It sniffs the format from the file's own bytes before believing the
extension — a model that arrived through a share sheet or a download often has
the wrong one.
| Format | Reader | Default unit |
|---|---|---|
| `.usdz` `.usda` `.usdc` `.usd` `.reality` | RealityKit | metres (already metric) |
| `.stl` | ModelIO | **millimetres** |
| `.obj` (+ `.mtl`) | ModelIO | metres |
| `.ply` | ModelIO | metres |
| `.3mf` | SceneViewSwift's own parser | **declared by the file** |
`.3mf` is parsed by SceneViewSwift, not by Apple — `MDLAsset` does not read it and
neither does Quick Look, even though it is the format 3D printing standardised
on. It is also the only mesh format here that states its own unit.
**glTF (`.glb` / `.gltf`) is NOT supported on Apple platforms.** Neither
RealityKit nor ModelIO reads it. Convert to USDZ (Reality Converter,
`usdzconvert`) or use the Android/web SDK. Do not emit Swift that loads a `.glb`.
**Always reason about the unit.** STL, OBJ and PLY store bare numbers with no
unit: a slicer STL means millimetres, a photogrammetry OBJ means metres.
RealityKit works in metres, so getting this wrong puts a 21 cm print in the room
at 210 m. `ModelFormat.carriesUnit` is `false` for exactly those three — that is
the signal to ask the user, or to offer a mm/cm/in picker.
One example per format — each compiles as written:
```swift
import SceneViewSwift
// USDZ — already metric, `unit:` is ignored.
let helmet = try await ModelNode.load("models/helmet.usdz")
// STL — millimetres by default, so a 210 mm print is 0.21 m in AR.
let printed = try await ModelNode.load(contentsOf: stlURL)
// OBJ + .mtl sidecar, authored in centimetres.
let panel = try await ModelNode.load(contentsOf: objURL, unit: .centimeters)
// PLY from a phone scanner, bundled with the app.
let bust = try await ModelNode.load("scans/bust.ply", unit: .meters)
// 3MF from a slicer — the file states its own unit, so pass none.
let plate = try await ModelNode.load(contentsOf: threeMFURL)
```
Measure before you render — parsing has no RealityKit dependency:
```swift
let asset = try MeshAsset.load(contentsOf: url)
print(asset.format, asset.unit, asset.triangleCount)
if let size = asset.boundsInMeters?.extents {
print("real size in metres:", size) // the "will it fit?" number
}
let node = try await ModelNode(asset) // then display it
```
Failures name the format: `ModelLoadingError.unsupportedFormat(fileExtension:)`
carries the extension the user actually tried, so surface it
("SceneView cannot open .fbx files yet") instead of a generic "load failed".
## The minimal correct AR example
Verified against `samples/ios-demo/.../ARPlacementDemo.swift`. `ARSceneView` is
a `UIViewRepresentable` — iOS only:
```swift
ARSceneView(
planeDetection: .horizontal, // .horizontal | .vertical | .both | .none
showPlaneOverlay: true,
showCoachingOverlay: true,
showPlacementReticle: true, // built-in smoothed placement cursor (#894)
// groundingShadows: true is the default — tap-placed models get a
// RealityKit contact shadow automatically (opt out with false).
onTapOnPlane: { position, arView in
let anchor = AnchorNode.world(position: position)
let cube = GeometryNode.cube(size: 0.1, color: .blue)
anchor.add(cube.entity)
arView.scene.addAnchor(anchor.entity)
}
)
.onSessionStarted { arView in /* session began */ }
```
## Critical rules (verified — do not break)
1. **`ModelNode.load(_:)` is `async throws`.** It is NOT a `remember*`-style
nullable. Call it inside a SwiftUI `.task { }` (or another async context)
and `try`/`try?` it. Store the result in `@State`.
2. **A unitless mesh format needs a `unit:`.** `.stl`, `.obj` and `.ply` carry no
unit; RealityKit is metric. Pass `unit:` when you know it, and default to the
format's own convention otherwise (mm for STL — see "Model formats and
units"). Never emit code that loads a `.glb` / `.gltf` on Apple platforms.
3. **RealityKit entities are `@MainActor`-isolated.** Never mutate an `Entity`
from `DispatchQueue.global()`. Use `await MainActor.run { }` to cross back.
SwiftUI `.task` already runs on the main actor for view work.
4. **`AnchorNode` factories are iOS-specific** — `AnchorNode.world(position:)`
and `AnchorNode.plane(alignment:minimumBounds:)`. This differs from Android,
where `AnchorNode` wraps a `com.google.ar.core.Anchor`. Do NOT translate the
Android `AnchorNode(anchor:)` shape to Swift.
5. **`GeometryNode` factories** are static methods: `.cube(size:color:)`,
`.sphere(radius:color:)`, `.cylinder(radius:height:color:)`,
`.plane(width:depth:color:)`, `.cone(height:radius:color:)`, `.torus(...)`,
`.capsule(...)`. There are NO `CubeNode` / `SphereNode` types — that naming
is Android-only.
6. **`LightNode` factories** are `.directional(color:intensity:castsShadow:)`,
`.point(color:intensity:attenuationRadius:)`,
`.spot(color:intensity:innerAngle:outerAngle:)`. Position/aim via the fluent
`.position(_:)` / `.lookAt(_:)` modifiers — not Android's `LightManager.Type`
enum + `apply` lambda.
7. **Some Android APIs do not port to RealityKit.** Before re-attacking a
deprecated symbol, consult the "iOS parity status (#1036)" tables in
`cheatsheet-ios.md`: `CameraNode.exposure`, `CameraNode.depthOfField`, and
`LightNode.shadowColor` are compile-warning no-ops on iOS;
`ARSceneView(playbackDataset:)`, `StreetscapeGeometry`, terrain/rooftop
anchors have no ARKit equivalent. `ARRecorder` on iOS is **record-only**
(ReplayKit screen capture) — there is no deterministic playback.
`SurfaceMirrorer` / `rememberSurfaceMirrorer()` (in-app scene→MP4 recording,
#2626) is **Android-only** — on iOS record the scene via that same ReplayKit path.
8. **`SceneView` is cross-platform (iOS/macOS/visionOS); `ARSceneView` is iOS
only.** macOS and visionOS get 3D but not the ARKit camera view.
9. **Cloud Anchors on iOS are a wrapper you complete — not a missing feature.**
`CloudAnchorNode.host(ttlDays:completion:operation:)` /
`.resolve(cloudAnchorId:completion:operation:)` and the cancellable
`CloudAnchorFuture` are REAL SceneViewSwift API. By design the core library
stays dependency-free: the billed `GARSession` round-trip is supplied by the
**app**, which adds Google's `arcore-ios-sdk` (SPM `google-ar/arcore-ios-sdk`,
product `ARCoreCloudAnchors`) and plugs it into the `operation:` closure.
Generate that app-side wiring — never tell a developer Cloud Anchors are
impossible on iOS. Caveat: the `ios-demo` app itself does NOT link
arcore-ios-sdk, so its `ar-cloud-anchor` screen shows a "Preview" badge with
Host / Resolve disabled (plane detection + tap-to-place stay live). See #2864.
## Performance / hot paths
**Don't drive SwiftUI `@State` from a per-frame loop** (an `onFrame` / RealityKit
`update` closure). A `@State` write every frame churns the view `body` — the
per-frame loop should mutate entities (or a reference-box `class` you hold) and only
flip `@State` when UI-visible state actually changes. Same root rule as the other
platforms: never recompute or allocate per frame what you can read once and cache.
Full cross-platform guidance:
[`docs/docs/performance.md` § Hot Paths & Allocation-Free APIs](https://github.com/sceneview/sceneview/blob/main/docs/docs/performance.md)
(audit umbrella [#2263](https://github.com/sceneview/sceneview/issues/2263)).
## Toolchain pairing
Pair this skill with Xcode's command-line tools:
- Build and run on the simulator — list the destinations available on the current
machine and select one by UDID instead of hardcoding an iPhone model name:
```bash
xcrun simctl list devices available
xcodebuild -scheme SceneViewSwift \
-destination 'platform=iOS Simulator,id=<UDID from the list above>'
```
- `xcrun simctl io booted screenshot ui.png` — capture the rendered scene.
- `swift build` / `swift test` from `SceneViewSwift/` — build/test the package
in isolation.
## Resources
- **[Cheat sheet](./references/cheatsheet.md)** — pointer to the canonical
`docs/docs/cheatsheet-ios.md` plus the most-used SwiftUI signatures.
- **[Recipes](./references/recipes.md)** — pointers to the working demo in
`samples/ios-demo/` for each canonical pattern. Read the demo, copy from it.
- **[Migration](./references/migration.md)** — SceneKit / RealityView →
SceneViewSwift, and the Android↔Apple mapping.
## Workflow guidance
When the user asks for a SceneViewSwift feature:
1. **Confirm the Apple platform.** iOS / macOS / visionOS — `ARSceneView` is
iOS-only; `SceneView` works everywhere.
2. **Pick the right entrypoint.** `SceneView { }` for 3D, `ARSceneView(…)` for
AR. Mention `import SceneViewSwift` and the SPM dependency line.
3. **Read the matching demo** under `samples/ios-demo/.../Demos/` before
writing code. Fall back to `cheatsheet-ios.md`. Never invent an API.
4. **`ModelNode.load` is async** — wrap it in `.task { }`, store in `@State`.
5. **Keep entity mutation on `@MainActor`.**
6. **For AR**, remind the user about `NSCameraUsageDescription` in `Info.plist`.
7. **If the user pastes Android Kotlin**, do NOT translate it character by
character — the result-builder + modifiers + factory naming differ. Use the
Android↔Apple mapping table in `cheatsheet-ios.md`.
8. **Before reusing a deprecated symbol**, check the iOS parity-status tables.
---
name: sceneview-ios
description: Build 3D and AR apps on Apple platforms (iOS, macOS, visionOS) with SceneViewSwift — the SwiftUI wrapper around RealityKit. Use whenever the user asks for "3D in SwiftUI", "AR with ARKit in SwiftUI", a model viewer for iOS, or any Apple-platform 3D/AR app where the dependency is the `SceneViewSwift` Swift Package from `github.com/sceneview/sceneview`. For Jetpack Compose / Android use the `sceneview` skill instead; for the browser use `sceneview-web`. Skip for plain ARKit-SDK / SceneKit / Unity / Unreal / RealityKit-without-SceneViewSwift work.
license: Apache-2.0
metadata:
author: SceneView
source: https://github.com/sceneview/sceneview
last-updated: '2026-10-02'
keywords:
- sceneview
- sceneviewswift
- 3d
- ar
- arkit
- realitykit
- swiftui
- model viewer
- augmented reality
- ios
- macos
- visionos
- swift package manager
- usdz
- stl
- obj
- ply
- mesh formats
- 3d printing
- 3mf
---
## What SceneViewSwift is
SceneViewSwift is the Apple-platform half of the SceneView SDK — a **declarative
SwiftUI** API over **RealityKit**. Same mental model as the Android library
(`SceneView { }` / `ARSceneView { }`) but expressed with SwiftUI result-builders
and view modifiers, not Jetpack Compose.
- **3D** — `SceneView { … }` SwiftUI view (iOS / macOS / visionOS).
- **AR** — `ARSceneView(…)` SwiftUI view (iOS only — ARKit).
- **Renderer** — RealityKit. There is **no Filament** on Apple platforms.
- **Distribution** — Swift Package Manager. The package lives in the
`github.com/sceneview/sceneview` monorepo; consumers add it by URL and pin a
version tag (currently `4.62.0`).
`SceneViewSwift` is consumable directly from Swift, and also underneath the
Flutter and React Native bridges.
## Authoritative API reference
Three sources, in priority order:
1. **`docs/docs/cheatsheet-ios.md`** in the repo — the complete Apple-platform
API reference (every node factory, view modifier, environment preset,
the Android↔Apple mapping table, and the iOS parity-status tables).
<https://github.com/sceneview/sceneview/blob/main/docs/docs/cheatsheet-ios.md>
2. **`SceneViewSwift/Sources/SceneViewSwift/`** — the actual Swift source. When
in doubt about a signature, read it. Node factories live under `Nodes/`,
the views in `SceneView.swift` / `ARSceneView.swift`.
3. **`samples/ios-demo/SceneViewDemo/Views/Demos/`** — a working SwiftUI demo
for every feature. Read the demo, do NOT improvise an API.
`llms.txt` at the repo root carries the cross-platform surface but is
Android-centric — prefer `cheatsheet-ios.md` for Swift.
## When to use this skill
Trigger on any of:
- "Build me a 3D viewer / AR app in SwiftUI."
- "Load a `.usdz` / `.reality` / `.stl` / `.obj` / `.ply` / `.3mf` model on iOS."
- "Open an STL from Files / show a 3D print at real size in AR."
- "Place a model on a detected AR plane in ARKit + SwiftUI."
- "Add 3D content to a visionOS / macOS app with SceneView."
- "Convert this SceneKit / RealityView code to SceneViewSwift."
- "Make `SceneViewer` from our Compose Multiplatform app actually render on iOS."
Skip for plain ARKit-SDK, SceneKit, Unity, Unreal, or RealityKit projects that
do NOT use the SceneViewSwift wrapper.
### If the ask comes from a Compose Multiplatform app
`sceneview-compose` exposes one `SceneViewer` composable from `commonMain`, but a Kotlin
Multiplatform module cannot depend on a Swift Package — so on iOS it renders a "no
renderer registered" notice until the **app** supplies one. That supply is a Swift task,
which is why it lands here:
1. Write an `@objc UIView` wrapping `SceneViewSwift.SceneView` in a
`UIHostingController`. The production-tested pattern is
`flutter/sceneview_flutter/ios/Classes/SceneViewPlugin.swift` — copy its retain-cycle
and Swift-6 actor handling rather than reinventing them.
2. Implement `SceneViewerViewFactory` (`create` **and** `update` — Kotlin/Native does not
bridge interface default bodies into the generated Obj-C protocol, so Swift must
implement both), and assign `SceneViewerBridge.factory` once before the first compose.
3. `update` must **mutate** the existing view. Recreating the scene reloads the model and
throws away the user's camera position.
4. Call the spec's `onCameraMoved` after every gesture — without it `CameraState` reports
only what the app last wrote — and `onError` when a load fails, since a failed load on
RealityKit looks exactly like a slow one.
`SceneViewerSpec` compares by value (model bytes by content), so `update` is **not**
called on every recomposition. Do not build Swift-side change caching that assumes it is.
SceneView does not ship that wrapper yet. If the user wants AR, custom materials or
post-processing, they are outside `sceneview-compose`'s scope — use `ARSceneView` /
`SceneView` from SceneViewSwift directly.
## Setup
```swift
// Package.swift
.package(url: "https://github.com/sceneview/sceneview.git", from: "4.62.0")
```
```swift
import SceneViewSwift
```
For AR, the host app's `Info.plist` must declare `NSCameraUsageDescription`.
For `ARRecorder` saving to Photos, add `NSPhotoLibraryAddUsageDescription`.
## The minimal correct 3D example
Verified against `samples/ios-demo/.../ModelViewerDemo.swift` and the
declarative `@NodeBuilder` initializer in `SceneView.swift`:
```swift
import SwiftUI
import SceneViewSwift
struct ModelViewerScreen: View {
@State private var model: ModelNode?
var body: some View {
SceneView { root in
if let model {
root.addChild(model.entity)
}
}
.environment(.studio) // IBL lighting preset
.cameraControls(.orbit) // .orbit | .pan | .firstPerson | native (iOS 18+): .none | .tilt | .dolly
.autoCenterContent(true)
.task {
model = try? await ModelNode.load("models/helmet.usdz")
model?.scaleToUnits(0.3)
model?.playAllAnimations()
}
}
}
```
The declarative form uses the `@NodeBuilder` result-builder — nodes are
expressions inside the trailing closure:
```swift
SceneView {
GeometryNode.cube(size: 0.3, color: .red)
.position(.init(x: -1, y: 0, z: -2))
GeometryNode.sphere(radius: 0.2, color: .blue)
.position(.init(x: 1, y: 0, z: -2))
}
.environment(.studio)
.cameraControls(.orbit)
```
## Model formats and units
`ModelNode.load(contentsOf:unit:)` is the **one entry point for every supported
format**. It sniffs the format from the file's own bytes before believing the
extension — a model that arrived through a share sheet or a download often has
the wrong one.
| Format | Reader | Default unit |
|---|---|---|
| `.usdz` `.usda` `.usdc` `.usd` `.reality` | RealityKit | metres (already metric) |
| `.stl` | ModelIO | **millimetres** |
| `.obj` (+ `.mtl`) | ModelIO | metres |
| `.ply` | ModelIO | metres |
| `.3mf` | SceneViewSwift's own parser | **declared by the file** |
`.3mf` is parsed by SceneViewSwift, not by Apple — `MDLAsset` does not read it and
neither does Quick Look, even though it is the format 3D printing standardised
on. It is also the only mesh format here that states its own unit.
**glTF (`.glb` / `.gltf`) is NOT supported on Apple platforms.** Neither
RealityKit nor ModelIO reads it. Convert to USDZ (Reality Converter,
`usdzconvert`) or use the Android/web SDK. Do not emit Swift that loads a `.glb`.
**Always reason about the unit.** STL, OBJ and PLY store bare numbers with no
unit: a slicer STL means millimetres, a photogrammetry OBJ means metres.
RealityKit works in metres, so getting this wrong puts a 21 cm print in the room
at 210 m. `ModelFormat.carriesUnit` is `false` for exactly those three — that is
the signal to ask the user, or to offer a mm/cm/in picker.
One example per format — each compiles as written:
```swift
import SceneViewSwift
// USDZ — already metric, `unit:` is ignored.
let helmet = try await ModelNode.load("models/helmet.usdz")
// STL — millimetres by default, so a 210 mm print is 0.21 m in AR.
let printed = try await ModelNode.load(contentsOf: stlURL)
// OBJ + .mtl sidecar, authored in centimetres.
let panel = try await ModelNode.load(contentsOf: objURL, unit: .centimeters)
// PLY from a phone scanner, bundled with the app.
let bust = try await ModelNode.load("scans/bust.ply", unit: .meters)
// 3MF from a slicer — the file states its own unit, so pass none.
let plate = try await ModelNode.load(contentsOf: threeMFURL)
```
Measure before you render — parsing has no RealityKit dependency:
```swift
let asset = try MeshAsset.load(contentsOf: url)
print(asset.format, asset.unit, asset.triangleCount)
if let size = asset.boundsInMeters?.extents {
print("real size in metres:", size) // the "will it fit?" number
}
let node = try await ModelNode(asset) // then display it
```
Failures name the format: `ModelLoadingError.unsupportedFormat(fileExtension:)`
carries the extension the user actually tried, so surface it
("SceneView cannot open .fbx files yet") instead of a generic "load failed".
## The minimal correct AR example
Verified against `samples/ios-demo/.../ARPlacementDemo.swift`. `ARSceneView` is
a `UIViewRepresentable` — iOS only:
```swift
ARSceneView(
planeDetection: .horizontal, // .horizontal | .vertical | .both | .none
showPlaneOverlay: true,
showCoachingOverlay: true,
showPlacementReticle: true, // built-in smoothed placement cursor (#894)
// groundingShadows: true is the default — tap-placed models get a
// RealityKit contact shadow automatically (opt out with false).
onTapOnPlane: { position, arView in
let anchor = AnchorNode.world(position: position)
let cube = GeometryNode.cube(size: 0.1, color: .blue)
anchor.add(cube.entity)
arView.scene.addAnchor(anchor.entity)
}
)
.onSessionStarted { arView in /* session began */ }
```
## Critical rules (verified — do not break)
1. **`ModelNode.load(_:)` is `async throws`.** It is NOT a `remember*`-style
nullable. Call it inside a SwiftUI `.task { }` (or another async context)
and `try`/`try?` it. Store the result in `@State`.
2. **A unitless mesh format needs a `unit:`.** `.stl`, `.obj` and `.ply` carry no
unit; RealityKit is metric. Pass `unit:` when you know it, and default to the
format's own convention otherwise (mm for STL — see "Model formats and
units"). Never emit code that loads a `.glb` / `.gltf` on Apple platforms.
3. **RealityKit entities are `@MainActor`-isolated.** Never mutate an `Entity`
from `DispatchQueue.global()`. Use `await MainActor.run { }` to cross back.
SwiftUI `.task` already runs on the main actor for view work.
4. **`AnchorNode` factories are iOS-specific** — `AnchorNode.world(position:)`
and `AnchorNode.plane(alignment:minimumBounds:)`. This differs from Android,
where `AnchorNode` wraps a `com.google.ar.core.Anchor`. Do NOT translate the
Android `AnchorNode(anchor:)` shape to Swift.
5. **`GeometryNode` factories** are static methods: `.cube(size:color:)`,
`.sphere(radius:color:)`, `.cylinder(radius:height:color:)`,
`.plane(width:depth:color:)`, `.cone(height:radius:color:)`, `.torus(...)`,
`.capsule(...)`. There are NO `CubeNode` / `SphereNode` types — that naming
is Android-only.
6. **`LightNode` factories** are `.directional(color:intensity:castsShadow:)`,
`.point(color:intensity:attenuationRadius:)`,
`.spot(color:intensity:innerAngle:outerAngle:)`. Position/aim via the fluent
`.position(_:)` / `.lookAt(_:)` modifiers — not Android's `LightManager.Type`
enum + `apply` lambda.
7. **Some Android APIs do not port to RealityKit.** Before re-attacking a
deprecated symbol, consult the "iOS parity status (#1036)" tables in
`cheatsheet-ios.md`: `CameraNode.exposure`, `CameraNode.depthOfField`, and
`LightNode.shadowColor` are compile-warning no-ops on iOS;
`ARSceneView(playbackDataset:)`, `StreetscapeGeometry`, terrain/rooftop
anchors have no ARKit equivalent. `ARRecorder` on iOS is **record-only**
(ReplayKit screen capture) — there is no deterministic playback.
`SurfaceMirrorer` / `rememberSurfaceMirrorer()` (in-app scene→MP4 recording,
#2626) is **Android-only** — on iOS record the scene via that same ReplayKit path.
8. **`SceneView` is cross-platform (iOS/macOS/visionOS); `ARSceneView` is iOS
only.** macOS and visionOS get 3D but not the ARKit camera view.
9. **Cloud Anchors on iOS are a wrapper you complete — not a missing feature.**
`CloudAnchorNode.host(ttlDays:completion:operation:)` /
`.resolve(cloudAnchorId:completion:operation:)` and the cancellable
`CloudAnchorFuture` are REAL SceneViewSwift API. By design the core library
stays dependency-free: the billed `GARSession` round-trip is supplied by the
**app**, which adds Google's `arcore-ios-sdk` (SPM `google-ar/arcore-ios-sdk`,
product `ARCoreCloudAnchors`) and plugs it into the `operation:` closure.
Generate that app-side wiring — never tell a developer Cloud Anchors are
impossible on iOS. Caveat: the `ios-demo` app itself does NOT link
arcore-ios-sdk, so its `ar-cloud-anchor` screen shows a "Preview" badge with
Host / Resolve disabled (plane detection + tap-to-place stay live). See #2864.
## Performance / hot paths
**Don't drive SwiftUI `@State` from a per-frame loop** (an `onFrame` / RealityKit
`update` closure). A `@State` write every frame churns the view `body` — the
per-frame loop should mutate entities (or a reference-box `class` you hold) and only
flip `@State` when UI-visible state actually changes. Same root rule as the other
platforms: never recompute or allocate per frame what you can read once and cache.
Full cross-platform guidance:
[`docs/docs/performance.md` § Hot Paths & Allocation-Free APIs](https://github.com/sceneview/sceneview/blob/main/docs/docs/performance.md)
(audit umbrella [#2263](https://github.com/sceneview/sceneview/issues/2263)).
## Toolchain pairing
Pair this skill with Xcode's command-line tools:
- Build and run on the simulator — list the destinations available on the current
machine and select one by UDID instead of hardcoding an iPhone model name:
```bash
xcrun simctl list devices available
xcodebuild -scheme SceneViewSwift \
-destination 'platform=iOS Simulator,id=<UDID from the list above>'
```
- `xcrun simctl io booted screenshot ui.png` — capture the rendered scene.
- `swift build` / `swift test` from `SceneViewSwift/` — build/test the package
in isolation.
## Resources
- **[Cheat sheet](./references/cheatsheet.md)** — pointer to the canonical
`docs/docs/cheatsheet-ios.md` plus the most-used SwiftUI signatures.
- **[Recipes](./references/recipes.md)** — pointers to the working demo in
`samples/ios-demo/` for each canonical pattern. Read the demo, copy from it.
- **[Migration](./references/migration.md)** — SceneKit / RealityView →
SceneViewSwift, and the Android↔Apple mapping.
## Workflow guidance
When the user asks for a SceneViewSwift feature:
1. **Confirm the Apple platform.** iOS / macOS / visionOS — `ARSceneView` is
iOS-only; `SceneView` works everywhere.
2. **Pick the right entrypoint.** `SceneView { }` for 3D, `ARSceneView(…)` for
AR. Mention `import SceneViewSwift` and the SPM dependency line.
3. **Read the matching demo** under `samples/ios-demo/.../Demos/` before
writing code. Fall back to `cheatsheet-ios.md`. Never invent an API.
4. **`ModelNode.load` is async** — wrap it in `.task { }`, store in `@State`.
5. **Keep entity mutation on `@MainActor`.**
6. **For AR**, remind the user about `NSCameraUsageDescription` in `Info.plist`.
7. **If the user pastes Android Kotlin**, do NOT translate it character by
character — the result-builder + modifiers + factory naming differ. Use the
Android↔Apple mapping table in `cheatsheet-ios.md`.
8. **Before reusing a deprecated symbol**, check the iOS parity-status tables.
COMPATIBILITY & REFERENCES
Platform guidance
Imported from the listed AgentSkill.sh category. Complete supplied package files and notices are retained. Required external tools and credentials are not installed. No bundled code was executed during import. Individual agent task behavior has not been tested.
Creates comprehensive frontend UI/UX designs including user interfaces, design systems, component libraries, responsive layouts, and accessibility implementations. Produces wireframes, mockups, design specifications, and implementation guidelines. Use when designing user interfaces, creating desi...
Expert in building scalable design systems with Atomic Design, design tokens, and theming infrastructure. Use when creating component libraries, implementing dark mode, or establishing typography and color systems. Covers multi-brand support, Storybook-driven development, and accessibility-first ...
Deep CSS analysis tool for understanding specificity, cascade order, inheritance chains, and computed styles. Visualizes CSS complexity and suggests simplifications. Use when debugging specificity issues or understanding CSS behavior.