Simulating particles in your visionOS app
At a glance
| Item | Summary |
|---|---|
| Purpose | Add a range of visual effects to a RealityKit view by attaching a particle emitter component to an entity. |
| App architecture | ParticleEmitterApp presents MainView; views create and tune particle-emitter entities while reusable gesture components and GeneralGoalSystem keep entity interaction outside the SwiftUI hierarchy. |
| Main patterns | SwiftUI scene composition, UI–RealityKit bridge, Entity-component-system, Entity-scoped particle configuration |
| Project style | Code-rich sample with 12 scanned Swift file(s) and 1328 implementation line(s). |
Project structure
Source bundle/
├── Particle Emitter/
│ ├── MainView.swift # MainView
│ ├── EmitterView.swift # EmitterView
│ ├── ParticleEmitterApp.swift # ParticleEmitterApp
│ └── EmitterSettings.swift # EmitterSettings, EmitterPresets, ColorSetting
└── Packages/
└── RealityGestures/
└── Sources/
└── RealityGestures/
├── RealityDragComponent.swift # RealityDragComponent, MoveConstraint, DragComponentType
├── GeneralGoalComponent.swift # GeneralGoalComponent, GeneralGoal, GeneralGoalSystem
└── RealityTapComponent.swift # RealityTapComponent
Structure observations
- The runtime boundary is WindowGroup; 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
ParticleEmitterApp_1["ParticleEmitterApp"]
WindowGroup_2["WindowGroup"]
MainView_3["MainView"]
GeneralGoalSystem_4["GeneralGoalSystem"]
RealityKit_entity_graph_5["RealityKit entity graph"]
ParticleEmitterComponent_6["ParticleEmitterComponent"]
ParticleEmitterApp_1 --> WindowGroup_2
WindowGroup_2 --> MainView_3
MainView_3 --> GeneralGoalSystem_4
GeneralGoalSystem_4 --> RealityKit_entity_graph_5
RealityKit_entity_graph_5 --> ParticleEmitterComponent_6
Reference code
Particle Emitter/ParticleEmitterApp.swift:11 — executable or app-lifecycle anchor.
struct ParticleEmitterApp: App {
/// Stores settings for the particle emitter.
@ObservedObject public var emitterSettings = EmitterSettings()
var body: some Scene {
WindowGroup {
MainView()
}.environment(emitterSettings)
}
}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
ParticleEmitterApp o-- EmitterSettings : emitterSettings
EmitterSettings *-- Entity : emitterEntity
MainView o-- EmitterSettings : emitterSettings
GeneralGoalSystem *-- EntityQuery : generalGoalQuery
Ownership evidence
Particle Emitter/ParticleEmitterApp.swift:13 — representative stored state or nearest verified lifecycle anchor.
@main
struct ParticleEmitterApp: App {
// ...
@ObservedObject public var emitterSettings = EmitterSettings()
// ...
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
ParticleEmitterApp |
EmitterSettings as emitterSettings |
receives a shared or non-owning reference | The upstream owner controls lifetime; this scope may invoke the exposed mutable API |
EmitterSettings |
Entity as emitterEntity |
creates and retains | The owning type coordinates writes |
MainView |
EmitterSettings as emitterSettings |
receives a shared or non-owning reference | The upstream owner controls lifetime; this scope may invoke the exposed mutable API |
GeneralGoalSystem |
EntityQuery as generalGoalQuery |
creates and retains | The owning type coordinates writes |
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 |
|---|---|---|
ParticleEmitterApp (Particle Emitter/ParticleEmitterApp.swift:11) |
Declares app lifecycle and top-level dependency lifetime. | App |
MainView (Particle Emitter/MainView.swift:14) |
Presents UI and forwards gestures or lifecycle events. | View |
GeneralGoalSystem (Packages/RealityGestures/Sources/RealityGestures/GeneralGoalComponent.swift:47) |
Updates entities matching a RealityKit component query. | System |
GeneralGoalComponent (Packages/RealityGestures/Sources/RealityGestures/GeneralGoalComponent.swift:13) |
Stores RealityKit entity data. | Component, Codable |
RealityDragComponent (Packages/RealityGestures/Sources/RealityGestures/RealityDragComponent.swift:13) |
Stores RealityKit entity data. | Component |
RealityTapComponent (Packages/RealityGestures/Sources/RealityGestures/RealityTapComponent.swift:30) |
Stores RealityKit entity data. | Component |
EmitterView (Particle Emitter/EmitterView.swift:13) |
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 var updated: ((EntityTargetValue<DragGesture.Value>, RealityDra... (Packages/RealityGestures/Sources/RealityGestures/SwiftUI+RealityGestures.swift:35) |
private |
Use stays inside the declaration and same-file extensions permitted by Swift. | Inference: Hide lifecycle-sensitive state or an implementation step. |
fileprivate static func getClampedPosition( (Packages/RealityGestures/Sources/RealityGestures/RealityDragComponent.swift:157) |
fileprivate |
Use is limited to declarations in this source file. | Inference: Share with same-file helpers without making the symbol module-wide. |
public func dragEnded(_ entity: Entity, worldPos: SIMD3<Float>) { (Packages/RealityGestures/Sources/RealityGestures/RealityDragComponent+DragEvents.swift:76) |
public |
The symbol is available to importing modules, subject to its containing type’s visibility. | Inference: Expose an intentional package or cross-target surface. |
internal func handleMoveState( (Packages/RealityGestures/Sources/RealityGestures/RealityDragComponent.swift:177) |
internal |
With no narrower modifier, Swift keeps the declaration internal to the module. | Inference: Allow app-target collaboration without exporting a library API. |
No reviewed Swift access example uses private(set), open; declarations without a modifier are internal.
Reference code
Packages/RealityGestures/Sources/RealityGestures/SwiftUI+RealityGestures.swift:35 — representative visibility boundary.
public struct RealityDragGesture: Gesture {
// ...
private var updated: ((EntityTargetValue<DragGesture.Value>, RealityDragComponent.DragStatus) -> Void)?
// ...
}Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Application/command lifecycle | ParticleEmitterApp |
The executable boundary determines app, controller, scene, or command lifetime. |
| Presentation and user intent | MainView |
The view/controller forwards input and owns only presentation-local work. |
| Framework/session/render coordination | GeneralGoalSystem |
The role-named collaborator isolates long-lived or per-frame work from presentation code. |
| Per-entity data and repeated behavior | Packages/RealityGestures/Sources/RealityGestures/GeneralGoalComponent.swift:47 |
Components carry data; the system queries and updates matching entities. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| SwiftUI scene composition | Particle Emitter/ParticleEmitterApp.swift:11 |
Keeps windows, volumes, and immersive presentation visible at the app boundary. |
| UI–RealityKit bridge | Particle Emitter/EmitterView.swift:28 |
Builds or hosts the RealityKit entity graph from SwiftUI or a platform view/controller lifecycle. |
| Entity-component-system | Packages/RealityGestures/Sources/RealityGestures/GeneralGoalComponent.swift:47 |
Stores per-entity data in components and advances repeated behavior in registered systems. |
| Entity-scoped particle configuration | Particle Emitter/EmitterSettings.swift:17 |
Keeps emitter parameters on the entity so views can tune effects without introducing a parallel simulation model. |
Naming conventions
- Role suffixes are evidence, not decoration: App:
ParticleEmitterApp; System:GeneralGoalSystem; Component:GeneralGoalComponent,RealityDragComponent,RealityTapComponent; View:EmitterView,MainView. - ECS names pair data and behavior: components
GeneralGoalComponent,RealityDragComponent,RealityTapComponent; systemsGeneralGoalSystem. - Protocols: no app-defined protocol in the reviewed source.
- Commands use verb-led methods:
buildEmitterContent,updateEmitterBounds,getCollisionPoints,getClampedPosition,getClampedRotation,handleMoveState,handleRotationState,closestPoint. - 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
ParticleEmitterAppas 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.
Source map
| Source file | Relevant symbols |
|---|---|
Particle Emitter/MainView.swift:14 |
MainView |
Particle Emitter/EmitterView.swift:13 |
EmitterView |
Particle Emitter/ParticleEmitterApp.swift:11 |
ParticleEmitterApp |
Packages/RealityGestures/Sources/RealityGestures/RealityDragComponent.swift:13 |
RealityDragComponent, MoveConstraint, DragComponentType, DragStatus, DragData |
Packages/RealityGestures/Sources/RealityGestures/GeneralGoalComponent.swift:13 |
GeneralGoalComponent, GeneralGoal, GeneralGoalSystem |
Packages/RealityGestures/Sources/RealityGestures/RealityTapComponent.swift:30 |
RealityTapComponent |
Particle Emitter/EmitterSettings.swift:16 |
EmitterSettings, EmitterPresets, ColorSetting |