Manipulating models with RealityKit
At a glance
| Item | Summary |
|---|---|
| Purpose | Interact with detailed 3D models using manipulation and clipping controls. |
| App architecture | ModelManipulatorApp owns AppModel for loaded entities and selection; views forward gestures while ClippingController and a sync component/system isolate clipping-volume behavior from UI state. |
| Main patterns | SwiftUI scene composition, UI–RealityKit bridge, Observable state owner, Entity-component-system, Async update consumer, Clipping-state adapter |
| Project style | Code-rich sample with 27 scanned Swift file(s) and 1786 implementation line(s). |
Project structure
Source bundle/
└── ModelManipulator/
├── Models/
│ ├── AppModel.swift # AppModel, ImmersiveSpaceState, ViewState
│ └── ClippingController.swift # ClippingController
├── Systems/
│ └── ClippingTransformSyncSystem.swift # ClippingTransformSyncSystem
├── ModelManipulatorApp.swift # ModelManipulatorApp
├── Views/
│ └── ContentView.swift # ContentView
└── Components/
├── ClippingBoundsCacheComponent.swift # ClippingBoundsCacheComponent, ClippingState
└── ClippingTransformSyncComponent.swift # ClippingTransformSyncComponent
Structure observations
- The runtime boundary is WindowGroup + ImmersiveSpace; the tree is pruned to lifecycle, state, framework, rendering, and ECS files.
- The source is Swift; role-named types and feature folders separate the major responsibilities.
Overall architecture
flowchart LR
ModelManipulatorApp_1["ModelManipulatorApp"]
WindowGroup___ImmersiveSpace_2["WindowGroup + ImmersiveSpace"]
ContentView_3["ContentView"]
AppModel_4["AppModel"]
ClippingController_5["ClippingController"]
RealityKit_entity_graph_6["RealityKit entity graph"]
RealityKit_components_and_systems_7["RealityKit components and systems"]
ModelManipulatorApp_1 --> WindowGroup___ImmersiveSpace_2
WindowGroup___ImmersiveSpace_2 --> ContentView_3
ContentView_3 --> AppModel_4
AppModel_4 --> ClippingController_5
ClippingController_5 --> RealityKit_entity_graph_6
RealityKit_entity_graph_6 --> RealityKit_components_and_systems_7
Reference code
ModelManipulator/ModelManipulatorApp.swift:11 — executable or app-lifecycle anchor.
struct ModelManipulatorApp: App {
// ...
}The diagram is a responsibility flow, not a claim that every adjacent node directly calls the next. It separates lifecycle, presentation, stable state, framework/session work, and the RealityKit entity graph.
Ownership and state
classDiagram
ModelManipulatorApp *-- AppModel : appModel
AppModel --> Entity : modelEntity
ContentView o-- AppModel : appModel
ModelManipulatorApp o-- ScenePhase : scenePhase
Ownership evidence
ModelManipulator/ModelManipulatorApp.swift:12 — representative stored state or nearest verified lifecycle anchor.
@State private var appModel = AppModel()
@Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
@Environment(\.dismissWindow) private var dismissWindow
@Environment(\.openImmersiveSpace) private var openImmersiveSpace
@Environment(\.openWindow) private var openWindow
@Environment(\.scenePhase) private var scenePhase| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
ModelManipulatorApp |
AppModel as appModel |
creates and retains | The declaring type controls writes |
AppModel |
Entity as modelEntity |
stores and coordinates | The owning type coordinates writes |
ContentView |
AppModel as appModel |
receives a shared or non-owning reference | The upstream owner controls lifetime; this scope may invoke the exposed mutable API |
ModelManipulatorApp |
ScenePhase as scenePhase |
receives a shared or non-owning reference | The upstream owner controls lifetime; this scope may invoke the exposed mutable API |
Ownership is intentionally narrow: environment, bindings, observed objects, weak links, and delegate callbacks do not prove lifetime ownership. Initialized stored services and wrapper-managed state do; entities added inside a RealityKit content closure belong to that entity graph.
Class and protocol design
| Type | Responsibility | Depends on or conforms to |
|---|---|---|
ModelManipulatorApp (ModelManipulator/ModelManipulatorApp.swift:11) |
Declares app lifecycle and top-level dependency lifetime. | App |
ContentView (ModelManipulator/Views/ContentView.swift:10) |
Presents UI and forwards gestures or lifecycle events. | View |
AppModel (ModelManipulator/Models/AppModel.swift:15) |
Owns feature state and domain transitions. | Concrete framework collaborators |
ClippingController (ModelManipulator/Models/ClippingController.swift:11) |
Coordinates long-lived framework or cross-view work. | Concrete framework collaborators |
ClippingBoundsCacheComponent (ModelManipulator/Components/ClippingBoundsCacheComponent.swift:10) |
Stores RealityKit entity data. | Component |
ClippingTransformSyncComponent (ModelManipulator/Components/ClippingTransformSyncComponent.swift:10) |
Stores RealityKit entity data. | Component |
ClippingTransformSyncSystem (ModelManipulator/Systems/ClippingTransformSyncSystem.swift:12) |
Updates entities matching a RealityKit component query. | System |
EntityHierarchyView (ModelManipulator/Views/EntityHierarchyView.swift:11) |
Presents UI and forwards gestures or lifecycle events. | View |
The reviewed source defines no local substitution protocol. Its App, View, delegate, renderer, ARKit, and RealityKit conformances are framework-facing contracts, so this document does not label the whole app protocol-oriented.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
private(set) var isModelExpanded = false (ModelManipulator/Models/AppModel.swift:28) |
private(set) |
The getter keeps its wider visibility, while writes stay in the declaring type and permitted same-file extensions. | Inference: Protect an invariant while allowing observation. |
private weak var clippedEntity: Entity? (ModelManipulator/Entities/ClippingControlEntity.swift:13) |
private |
Use stays inside the declaration and same-file extensions permitted by Swift. | Inference: Hide lifecycle-sensitive state or an implementation step. |
No reviewed Swift access example uses fileprivate, public, open; declarations without a modifier are internal.
Reference code
ModelManipulator/Models/AppModel.swift:28 — representative visibility boundary.
private(set) var isModelExpanded = false
/// A Boolean value that indicates whether the model supports expansion into component parts.
var isModelExpandable: Bool { modelEntity?.assemblyParent != nil }Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Application/command lifecycle | ModelManipulatorApp |
The executable boundary determines app, controller, scene, or command lifetime. |
| Presentation and user intent | ContentView |
The view/controller forwards input and owns only presentation-local work. |
| Shared feature state and commands | AppModel |
A stable owner prevents view recomputation or callbacks from duplicating transitions. |
| Framework/session/render coordination | ClippingController |
The role-named collaborator isolates long-lived or per-frame work from presentation code. |
| Per-entity data and repeated behavior | ModelManipulator/Systems/ClippingTransformSyncSystem.swift:12 |
Components carry data; the system queries and updates matching entities. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| SwiftUI scene composition | ModelManipulator/ModelManipulatorApp.swift:11 |
Keeps windows, volumes, and immersive presentation visible at the app boundary. |
| UI–RealityKit bridge | ModelManipulator/Views/ImmersiveView.swift:21 |
Builds or hosts the RealityKit entity graph from SwiftUI or a platform view/controller lifecycle. |
| Observable state owner | ModelManipulator/Models/AppModel.swift:15 |
Keeps shared feature state in a stable owner rather than in transient view values. |
| Entity-component-system | ModelManipulator/Systems/ClippingTransformSyncSystem.swift:12 |
Stores per-entity data in components and advances repeated behavior in registered systems. |
| Async update consumer | ModelManipulator/Models/AppModel.swift:136 |
Consumes long-lived framework output in a cancellable asynchronous task. |
| Clipping-state adapter | ModelManipulator/Models/AppModel.swift:43 |
Moves clipping-resource details out of SwiftUI and synchronizes per-entity transform state through ECS. |
Naming conventions
- Role suffixes are evidence, not decoration: App:
ModelManipulatorApp; Model:AppModel; Controller:ClippingController; System:ClippingTransformSyncSystem; Component:ClippingBoundsCacheComponent,ClippingTransformSyncComponent. - ECS names pair data and behavior: components
ClippingBoundsCacheComponent,ClippingTransformSyncComponent; systemsClippingTransformSyncSystem. - Protocols: no app-defined protocol in the reviewed source.
- Commands use verb-led methods:
handleClippingStateChange,presentFileImporter,reset,selectDefaultModel,selectModel,setupClippingObservation,toggleModelExpansion,toggleModelLock. - Files and folders use primary-type or responsibility names such as
Views,Models,Rendering,Components,Systems,Packages, or feature names where those boundaries exist.
Architecture takeaways
- Treat
ModelManipulatorAppas the owner of executable or scene lifecycle, not automatically as the owner of every entity created later. - Keep view/controller input near presentation, but move sessions, reconstruction, capture, rendering, shared game state, or documents into stable owners when their lifetime exceeds one callback or render pass.
- Use components for per-entity data and systems for repeated simulation instead of centralizing every update in a view model or controller.
- Tie asynchronous session/output consumers to an explicit owner so cancellation and teardown follow the same lifecycle.
Source map
| Source file | Relevant symbols |
|---|---|
ModelManipulator/Models/AppModel.swift:15 |
AppModel, ImmersiveSpaceState, ViewState |
ModelManipulator/Models/ClippingController.swift:11 |
ClippingController |
ModelManipulator/Systems/ClippingTransformSyncSystem.swift:12 |
ClippingTransformSyncSystem |
ModelManipulator/ModelManipulatorApp.swift:11 |
ModelManipulatorApp |
ModelManipulator/Views/ContentView.swift:10 |
ContentView |
ModelManipulator/Components/ClippingBoundsCacheComponent.swift:10 |
ClippingBoundsCacheComponent, ClippingState |
ModelManipulator/Components/ClippingTransformSyncComponent.swift:10 |
ClippingTransformSyncComponent |