Creating animation graphs with Reality Composer Pro and RealityKit
At a glance
| Item | Summary |
|---|---|
| Purpose | Animate RealityKit entities using blend spaces and state machines with the Animation Graph feature. |
| App architecture | One WindowGroup hosts ContentView. That view loads the exported world scene, finds the owl_model entity, and mirrors two SwiftUI state values into the entity’s animation-graph parameters, moving and speed. The blend and state logic lives in the Reality Composer Pro animation graph, not in Swift. |
| Main patterns | Binding-based state propagation. ContentView owns @State values that a Toggle and a Slider edit through bindings. |
| Project style | A single application target backed by a file-system-synchronized AnimationGraph/ folder (Views/, Supporting Files/). It has no package dependencies, and the Reality Composer Pro project sits beside the Xcode project. |
| Execution model | The target defaults to MainActor isolation in Swift 6 language mode. The only suspension point is try await Entity(named:) inside the RealityView make closure. The source contains no Task, dispatch queue, or Swift System. |
| State/event model | SwiftUI @State plus Binding. .onChange(of:) writes each new value into Entity.parameters. |
| Key frameworks/packages | os, SwiftUI, RealityKit imports. The project declares no package references. |
Project structure
AnimationGraph.xcodeproj/ one app target, no package references
AnimationGraph/ synchronized root group (Info.plist excluded)
├── AnimationGraphApp.swift @main App: one WindowGroup, volumetric on visionOS
├── Views/
│ └── ContentView.swift OwlConstants, logger, ContentView
├── AnimationGraph.reality exported scene the app loads at run time
├── Info.plist volumetric default scene role
└── Supporting Files/Assets.xcassets
AnimationGraph.realitycomposerpro/ authoring project (not in the target)
├── Owl Animation Graph.tm_animation_graph
├── world.tm_entity
└── Animations/ fly, hover, perch clips
Configuration/SampleCode.xcconfig
Structure observations
- The Xcode project has one app target, and its
packageReferenceslist is empty (AnimationGraph.xcodeproj/project.pbxproj:134). There is no local Swift package and noPackage.swift. AnimationGraph/is a synchronized root group.Info.plistis listed in its membership exceptions, which excludes the file from the target’s synchronized membership (AnimationGraph.xcodeproj/project.pbxproj:24). Instead, the file is referenced throughINFOPLIST_FILE(AnimationGraph.xcodeproj/project.pbxproj:299).- Swift files are grouped by role: the entry point sits at the folder root and the only view sits in
Views/. The exportedAnimationGraph.realitysits inside the synchronized folder. Inference: that placement is what letsEntity(named:)load the scene from the app bundle. - The authoring project holds the behavior.
world.tm_entitycontains anowl_modelentity with atm_re_animation_graph_component.Owl Animation Graph.tm_animation_graphdefines parameters namedmovingandspeed, the same strings thatOwlConstantsuses in Swift.
Platform boundary
Apple’s documentation lists iOS, iPadOS, Mac Catalyst, macOS, and visionOS. The project’s supported platforms also include appletvos, with device family 3 (AnimationGraph.xcodeproj/project.pbxproj:332), so tvOS is added to the front matter. The project sets no SUPPORTS_MACCATALYST key and includes macosx, so the downloaded project targets native macOS. Mac Catalyst is kept in the front matter because Apple’s documentation lists it.
Overall architecture
flowchart LR
App["AnimationGraphApp"] --> WG["WindowGroup (volumetric on visionOS)"]
WG --> CV["ContentView"]
CV -->|await| Load["Entity(named: world)"]
Load --> Owl["owl_model Entity"]
Controls["Toggle / Slider"] -->|Binding| State["@State moving, speed"]
State -->|onChange| Params["owl.parameters"]
Params --> Graph["Animation graph (asset)"]
Owl --- Params
Reference code
AnimationGraph/AnimationGraphApp.swift:10 — AnimationGraphApp
@main
struct AnimationGraphApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
#if os(visionOS)
.windowStyle(.volumetric)
.defaultSize(width: 0.3, height: 0.25, depth: 0.3, in: .meters)
#endif
}
}Interpretation
Observed: the app is a single scene with a single view. ContentView combines asset loading, entity lookup, the UI controls, and the bridge from SwiftUI state to entity parameters. It does not use a separate model object. The animation graph asset owns the blending and the state-machine transitions. Swift only writes the graph’s input parameters.
Inference: the sample is meant to teach the parameter boundary. Swift drives a small number of named inputs, and the designer-authored graph turns them into animation. That keeps animation logic out of code.
Ownership and state
classDiagram
AnimationGraphApp *-- ContentView : creates in WindowGroup
ContentView *-- moving : @State Bool
ContentView *-- speed : @State Float
ContentView o-- Entity : @State owl (found in loaded scene)
Toggle --> moving : Binding
Slider --> speed : Binding
Ownership evidence
AnimationGraph/Views/ContentView.swift:22 — ContentView state
struct ContentView: View {
@State var owl: Entity?
@State var moving = false
@State var speed: Float = 0.0
// ...
@ViewBuilder
var animationControls: some View {
Toggle("Moving", isOn: $moving)
Slider(value: $speed, in: 0...1) {
Text("Speed")
}.frame(idealWidth: 200.0)
}
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
ContentView |
moving: Bool |
@State. A Toggle edits it through $moving |
Toggle via its binding |
ContentView |
speed: Float |
@State. A Slider edits it through $speed, limited to 0...1 |
Slider via its binding |
ContentView |
owl: Entity? |
@State reference to an entity inside the loaded scene. Assigned once in the make closure (AnimationGraph/Views/ContentView.swift:36) |
The make closure assigns it. The onChange handlers read it to write parameters |
RealityViewContent |
mainScene |
Added with content.add(mainScene). The view keeps no reference to the scene root |
RealityKit |
owl entity |
parameters["moving"], parameters["speed"] |
Seeded with BindableValue and then updated through .value |
ContentView |
| Module scope | logger |
A file-level let os.Logger (AnimationGraph/Views/ContentView.swift:19) |
Immutable |
Observed: SwiftUI state is the source of truth for both parameters. Updates flow one way, from SwiftUI to the entity. No code reads parameter values back from the graph.
Concurrency, scheduling, and thread safety
Evidence limit: actor isolation, async/await, or Task creation does not by itself prove background-thread execution; Sendable conformance alone does not prove thread-safe mutation.
| Concern | Source mechanism | Verified placement or handoff | Evidence |
|---|---|---|---|
| Main-actor isolation | Build setting SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, with SWIFT_APPROACHABLE_CONCURRENCY = YES and Swift 6 language mode |
Declarations in the app target that carry no annotation default to main-actor isolation. No source file writes @MainActor or MainActor.run explicitly |
AnimationGraph.xcodeproj/project.pbxproj:334 |
| Main-queue scheduling | None | No DispatchQueue appears in the source |
AnimationGraph/Views/ContentView.swift:28 |
| Task or worker execution | RealityView async make closure |
Suspends at try await Entity(named: OwlConstants.worldEntity), then assigns self.owl and seeds parameters after the load resumes. RealityView creates the task that runs this closure, and the source does not show its executor or any cancellation |
AnimationGraph/Views/ContentView.swift:32 |
| State-to-entity handoff | .onChange(of: moving), .onChange(of: speed) |
Synchronous view-update callbacks that write owl?.parameters[...]?.value. Optional chaining makes the write a no-op until owl is set |
AnimationGraph/Views/ContentView.swift:48 |
| Shared-state safety | SwiftUI @State within a main-actor-isolated view (default isolation) |
owl, moving, and speed are mutated only from the view’s closures. The source contains no lock or queue |
AnimationGraph/Views/ContentView.swift:22 |
| Per-frame scheduling | Not visible in Swift | The source declares no RealityKit System and no update(context:) method. Animation evaluation belongs to the asset’s animation graph component, and the sample does not show how it is scheduled |
AnimationGraph/Views/ContentView.swift:43 |
Task retention and cancellation: the sample creates no task, so it has nothing to retain or cancel. Load failures are logged with logger.error (AnimationGraph/Views/ContentView.swift:46).
Ordering detail (observed): if the user changes a control before the load finishes, the onChange write does nothing because owl is still nil. Once the load completes, the parameters are seeded from the current moving and speed values (BindableValue(moving)). Inference: the code that runs after the load reads the current state, so early edits are not lost.
Reference code
AnimationGraph/Views/ContentView.swift:28 — load, seed, and update
struct ContentView: View {
// ...
var body: some View {
VStack {
RealityView { content in
do {
// ...
let mainScene = try await Entity(named: OwlConstants.worldEntity)
content.add(mainScene)
// ...
self.owl = mainScene.findEntity(named: OwlConstants.owlName)
guard let owl else {
logger.error("\(OwlConstants.owlName) was not in scene \(OwlConstants.worldEntity)")
return
}
// ...
owl.parameters[OwlConstants.movingParameter] = BindableValue(moving)
owl.parameters[OwlConstants.speedParameter] = BindableValue(speed)
} catch {
logger.error("Failed to load scene: \(error)")
}
}.onChange(of: moving) {
owl?.parameters[OwlConstants.movingParameter]?.value = moving
}.onChange(of: speed) {
owl?.parameters[OwlConstants.speedParameter]?.value = speed
}
// ...
}
}
}State propagation, frameworks, and dependencies
Evidence limit: an import proves a source-level compilation dependency at the cited line; it does not prove runtime use, architectural adoption, or whether a Swift package is a direct application dependency.
| Category | Mechanism or module | Verified role | Evidence |
|---|---|---|---|
| State propagation | SwiftUI Binding ($moving, $speed) |
Controls write into @State owned by ContentView |
AnimationGraph/Views/ContentView.swift:64 |
| State propagation | .onChange(of:) to Entity.parameters[...]?.value |
Pushes SwiftUI state into the animation graph’s named parameters | AnimationGraph/Views/ContentView.swift:49 |
| State propagation | BindableValue |
Creates the parameter entries with initial values once the owl is found | AnimationGraph/Views/ContentView.swift:43 |
| Framework import | os |
Used for os.Logger |
AnimationGraph/Views/ContentView.swift:8 |
| Framework import | SwiftUI |
Imported in both Swift files | AnimationGraph/AnimationGraphApp.swift:8 |
| Framework import | RealityKit |
Used for RealityView, Entity, BindableValue, and realityViewCameraControls |
AnimationGraph/Views/ContentView.swift:10 |
| Package declaration | None | The project’s packageReferences list is empty |
AnimationGraph.xcodeproj/project.pbxproj:134 |
Class and protocol design
The sample declares three types and no local protocols, classes, RealityKit Component types, or System types. A class diagram would not add information.
AnimationGraph/Views/ContentView.swift:12 — OwlConstants
enum OwlConstants {
static let worldEntity = "world"
static let owlName = "owl_model"
static let movingParameter = "moving"
static let speedParameter = "speed"
}| Type | Responsibility | Depends on |
|---|---|---|
AnimationGraphApp |
Declares the single window scene and its volumetric style on visionOS | App, WindowGroup |
ContentView |
Loads the scene, finds the owl, owns control state, and writes animation parameters | View, RealityView, Entity, OwlConstants, logger |
OwlConstants |
A caseless enum used as a namespace for the asset-contract strings: entity names and parameter names |
None |
Observed: OwlConstants is a caseless enum, so it cannot be instantiated. It collects every string that must match the Reality Composer Pro project in one place.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
OwlConstants and its static let members |
implicit internal |
Visible across the app module | Inference: a single-target app has no public API |
logger |
implicit internal file-level let |
Visible from any file in the module, not only ContentView.swift |
Inference: available for reuse; private would limit it to this file |
ContentView.owl, moving, speed |
implicit internal @State var |
Not marked private, although the SwiftUI convention is to keep @State private to the owning view |
Inference: sample brevity. Nothing outside the file reads them |
animationControls |
implicit internal computed property |
A @ViewBuilder helper split out of body |
Inference: readability |
Reference code
AnimationGraph/Views/ContentView.swift:19 — module-wide logger
let logger = os.Logger(subsystem: "com.example.apple-samplecode", category: "AnimationGraph")The source contains no explicit private, fileprivate, public, or open modifier. Every boundary is the default module-internal scope.
Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| Scene declaration and volumetric sizing | AnimationGraphApp |
Platform-specific window style under #if os(visionOS) (AnimationGraph/AnimationGraphApp.swift:17) |
| Asset loading and entity lookup | ContentView make closure |
Runs once when the RealityView is created and logs on failure |
| Control state | ContentView @State |
Local UI state with no model layer |
| Bridge from state to parameters | ContentView .onChange handlers |
Write the value whenever it changes, with a nil-safe guard |
| Camera controls on non-visionOS platforms | ContentView |
.realityViewCameraControls(.orbit) under #if !os(visionOS) (AnimationGraph/Views/ContentView.swift:57) |
| Blending, state machine, clip selection | Owl Animation Graph.tm_animation_graph (asset) |
Authored in Reality Composer Pro and evaluated by RealityKit |
| Contract strings shared by code and asset | OwlConstants |
A single place to check when asset names change |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Binding-based state propagation | AnimationGraph/Views/ContentView.swift:64 (Toggle(isOn: $moving)) and AnimationGraph/Views/ContentView.swift:65 (Slider(value: $speed, ...)) |
ContentView remains the authoritative owner. The controls get write access through bindings, and onChange forwards each new value to the entity |
Removed from consideration: no MVVM, store, or observable model exists, and no ECS System is declared in Swift. The parameter bridge is direct, view-owned code, so it gets no separate pattern label.
Naming conventions
- Types:
AnimationGraphAppfollows the<ProductName>Appconvention.ContentViewis the template root view name.OwlConstantsfollows the<Domain>Constantsconvention for a namespace enum. - Constants: names describe their role in the asset contract. Entity names end in
EntityorName(worldEntity,owlName), and graph inputs end inParameter(movingParameter,speedParameter). The values are the exact asset identifiers ("owl_model","moving"). - Properties: state uses plain nouns and adjectives (
owl,moving,speed). The control group is a computed property named for its content (animationControls). - Files and folders: the entry point is at the root and views go in
Views/. Resources go inSupporting Files/. - Protocols: none declared.
Architecture takeaways
- Treat an animation graph’s parameters as an interface between code and asset. Swift writes a few named inputs, and the designer-authored graph owns blending and transitions.
- Put every string the code shares with the asset in one namespace (
OwlConstants), so renaming an asset changes code in one place. - Seed parameters after the async load from the current state, then push changes with
onChange. Optional chaining handles changes made before the entity exists. - State flows one way: control, then binding, then
@State, thenonChange, then the entity parameter. No code reads values back from the graph. - Concurrency is framework-owned.
RealityViewcreates the only async context, and the target’s defaultMainActorisolation covers the view’s state.
Source map
| Source file | Relevant symbols |
|---|---|
AnimationGraph/AnimationGraphApp.swift |
AnimationGraphApp, WindowGroup, .windowStyle(.volumetric) |
AnimationGraph/Views/ContentView.swift |
OwlConstants, logger, ContentView, RealityView, BindableValue, onChange(of:), animationControls |
AnimationGraph.xcodeproj/project.pbxproj |
App target, synchronized root group exception, empty packageReferences, concurrency build settings, supported platforms |