Canyon Crosser: Building a volumetric hike-planning app
At a glance
| Item | Summary |
|---|---|
| Purpose | Create a hike planning app using SwiftUI and RealityKit. |
| App architecture | CanyonCrosserApp composes WindowGroup + volumetric window around ContentView; AppModel holds shared experience state and RealityKit components and systems own per-entity behavior. |
| Main patterns | SwiftUI scene composition, SwiftUI–RealityKit bridge, Observable state owner, Entity-component-system, Protocol capability boundary |
| Project style | Code-rich sample with 85 scanned Swift file(s) and 5099 Swift line(s); resources and generated assets are excluded from those counts. |
Project structure
Source bundle/
├── CanyonCrosser/CanyonCrosserApp.swift # CanyonCrosserApp
├── CanyonCrosser/Views/Progress Slider/WeatherOrTimeView.swift # WeatherOrTimeView, Display, WeatherAndTime
├── CanyonCrosser/Models/AppModel/AppModel.swift # LoadingError, AppModel
├── CanyonCrosser/Models/AppPhaseModel.swift # AppPhaseModel, AppPhase
├── CanyonCrosser/ContentView.swift # ContentView
├── Packages/RealityKitContent/Sources/RealityKitContent/ValueGradient.swift # Interpolatable, ValueGradient
├── CanyonCrosser/ECS/ClippingMarginPercentageComponent.swift # ClippingMarginPercentageComponent, Environment, Values
├── CanyonCrosser/ECS/FeatheringSystem.swift # FadingCloudComponent, FeatheringSystem
└── Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json # authored RealityKit content
Structure observations
- The runtime boundary is WindowGroup + volumetric window; the pruned tree lists only files that explain lifecycle, state, or framework integration.
- App code is split into role-named views, models, managers, providers, components, or systems.
- Authored
.realityor Reality Composer Pro content is a real implementation boundary; Swift loads or drives it rather than reproducing its entity graph.
Overall architecture
flowchart LR
CanyonCrosserApp_1["CanyonCrosserApp"]
WindowGroup___volumetric_window_2["WindowGroup + volumetric window"]
ContentView_3["ContentView"]
AppModel_4["AppModel"]
RealityView_entity_graph_5["RealityView entity graph"]
RealityKit_components_and_systems_6["RealityKit components and systems"]
CanyonCrosserApp_1 --> WindowGroup___volumetric_window_2
WindowGroup___volumetric_window_2 --> ContentView_3
ContentView_3 --> AppModel_4
AppModel_4 --> RealityView_entity_graph_5
RealityView_entity_graph_5 --> RealityKit_components_and_systems_6
Reference code
CanyonCrosser/CanyonCrosserApp.swift:12 — the app or executable entry declares the outer scene lifecycle.
struct CanyonCrosserApp: App {
// ...
}The diagram is a responsibility flow, not a claim that every adjacent node directly calls the next. It keeps scene ownership, shared state, RealityKit content, and framework-provider work at separate levels.
Ownership and state
classDiagram
CanyonCrosserApp *-- AppModel : appModel
CanyonCrosserApp *-- AppPhaseModel : appPhaseModel
ContentView o-- AppPhaseModel : appPhaseModel
AppModel --> AnimationPlaybackController : hikerAnimationController
Ownership evidence
CanyonCrosser/CanyonCrosserApp.swift:13 — representative stored state or the nearest verified lifecycle anchor.
@State private var appModel: AppModel = AppModel()| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
CanyonCrosserApp |
AppModel as appModel |
creates and retains | Only the declaring scope writes |
CanyonCrosserApp |
AppPhaseModel as appPhaseModel |
creates and retains | Only the declaring scope writes |
ContentView |
AppPhaseModel as appPhaseModel |
receives a shared/non-owning reference | The upstream owner controls lifetime; this scope may invoke its mutable API |
AppModel |
AnimationPlaybackController as hikerAnimationController |
stores and coordinates | The owning type coordinates writes |
Ownership here is deliberately narrow: @Environment and weak references are shared links, initialized @State or stored services are lifecycle ownership, and a RealityView content closure owns additions to its entity graph without making the SwiftUI view a reference-type owner.
Class and protocol design
| Type | Responsibility | Depends on or conforms to |
|---|---|---|
CanyonCrosserApp (CanyonCrosser/CanyonCrosserApp.swift:12) |
Declares app scenes and top-level dependency lifetime. | App |
ContentView (CanyonCrosser/ContentView.swift:11) |
Presents UI and forwards gestures or lifecycle events. | View |
AppModel (CanyonCrosser/Models/AppModel/AppModel.swift:21) |
Owns observable feature state and domain transitions. | Sendable |
Interpolatable (Packages/RealityKitContent/Sources/RealityKitContent/ValueGradient.swift:8) |
Declares a replaceable capability or lifecycle contract. | Concrete framework collaborators |
ClippingMarginPercentageComponent (CanyonCrosser/ECS/ClippingMarginPercentageComponent.swift:11) |
Stores RealityKit entity data. | Component |
FadingCloudComponent (CanyonCrosser/ECS/FeatheringSystem.swift:11) |
Stores RealityKit entity data. | Component |
FeatheringSystem (CanyonCrosser/ECS/FeatheringSystem.swift:13) |
Updates matching RealityKit entities. | System |
Verified protocol-oriented seams: Double → Interpolatable, Float → Interpolatable, SIMD3<Float> → Interpolatable, SIMD4<Float> → Interpolatable. Framework conformances remain adapters, not proof of an app-wide protocol architecture.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
@State private var appModel: AppModel = AppModel() (CanyonCrosser/CanyonCrosserApp.swift:13) |
private |
Use is restricted to the declaration and same-file extensions permitted by Swift. | Inference: Hide implementation details and lifecycle-sensitive state. |
fileprivate mutating func set(name: String, value newValue: MaterialPar... (CanyonCrosser/ECS/FeatheringSystem.swift:44) |
fileprivate |
Use is restricted to declarations in this source file. | Inference: Share a helper across same-file extensions without making it module-wide. |
public let trailEntityPath: String (CanyonCrosser/Models/Hike.swift:16) |
public |
The declaration is available to importing modules, subject to its containing type’s visibility. | Inference: Satisfy a cross-target or framework-facing surface without implying subclassability. |
No reviewed declaration uses private(set), open; unmodified Swift declarations are internal.
Reference code
CanyonCrosser/CanyonCrosserApp.swift:13 — representative visibility boundary.
@State private var appModel: AppModel = AppModel()Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Scene declaration and dependency lifetime | CanyonCrosserApp |
The App/entry boundary determines window, volume, and immersive-space lifetime. |
| Presentation, attachments, and gestures | ContentView |
SwiftUI view code forwards user intent and RealityView lifecycle events. |
| Shared feature state and commands | AppModel |
A role-named owner prevents sibling views from duplicating transitions. |
| Per-frame entity behavior | CanyonCrosser/ECS/FeatheringSystem.swift:13 |
RealityKit systems query component data instead of centralizing every entity update in SwiftUI. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| SwiftUI scene composition | CanyonCrosser/CanyonCrosserApp.swift:12 |
Keeps windows, volumes, and immersive-space lifecycle visible at the app boundary. |
| SwiftUI–RealityKit bridge | CanyonCrosser/Views/Grand Canyon/GrandCanyonView.swift:63 |
Builds and updates a RealityKit entity graph from SwiftUI lifecycle closures. |
| Observable state owner | CanyonCrosser/Models/AppModel/AppModel.swift:21 |
Shares feature state across multiple views without moving framework resources into view values. |
| Entity-component-system | CanyonCrosser/ECS/FeatheringSystem.swift:13 |
Stores per-entity data in components and advances behavior in registered RealityKit systems. |
| Protocol capability boundary | Packages/RealityKitContent/Sources/RealityKitContent/ValueGradient.swift:8 |
Defines a local capability used by multiple concrete types; this is the verified protocol-oriented seam. |
Naming conventions
- Role suffixes are evidence, not decoration: App:
CanyonCrosserApp; Model:AppModel,AppPhaseModel,CarouselModel; View:BackButtonView,CarouselBodyView,CarouselLabelView,CarouselView,CompactControlsView. - ECS names pair data and behavior: components
ClippingMarginPercentageComponent,FadingCloudComponent,HikePlaybackStateComponent,HikeTimingComponent; systemsFeatheringSystem,HikeSystem,LightRotationSystem,TimeOfDaySystem. - Protocols:
Interpolatable. - Commands use verb-led methods:
fadeInClouds,fadeOutClouds,getHikerAnimationPlaybackController,setHikerVisibility. - Files generally match their primary type;
Views,Models,Managers,Providers,Components,Systems, andPackagesfolders describe architectural roles where present.
Architecture takeaways
- Treat
CanyonCrosserAppas the owner of scene declarations, not as the owner of every RealityKit entity created later. - Keep view-local interaction in SwiftUI, but move provider sessions, playback resources, shared game state, or transport state into a stable owner when their lifetime exceeds one render pass.
- Use RealityKit components for per-entity data and systems for repeated simulation instead of a monolithic view model.
Source map
| Source file | Relevant symbols |
|---|---|
CanyonCrosser/CanyonCrosserApp.swift:12 |
CanyonCrosserApp |
CanyonCrosser/Views/Progress Slider/WeatherOrTimeView.swift:10 |
WeatherOrTimeView, Display, WeatherAndTime, TimeLabel, WeatherLabel |
CanyonCrosser/Models/AppModel/AppModel.swift:12 |
LoadingError, AppModel |
CanyonCrosser/Models/AppPhaseModel.swift:11 |
AppPhaseModel, AppPhase |
CanyonCrosser/ContentView.swift:11 |
ContentView |
Packages/RealityKitContent/Sources/RealityKitContent/ValueGradient.swift:8 |
Interpolatable, ValueGradient |
CanyonCrosser/ECS/ClippingMarginPercentageComponent.swift:11 |
ClippingMarginPercentageComponent, Environment, Values |
CanyonCrosser/ECS/FeatheringSystem.swift:11 |
FadingCloudComponent, FeatheringSystem |