Sample CodeiOS, iPadOS, Mac Catalyst, macOS, tvOS, visionOSReviewed 2026-11-01View on Apple Developer

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 packageReferences list is empty (AnimationGraph.xcodeproj/project.pbxproj:134). There is no local Swift package and no Package.swift.
  • AnimationGraph/ is a synchronized root group. Info.plist is 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 through INFOPLIST_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 exported AnimationGraph.reality sits inside the synchronized folder. Inference: that placement is what lets Entity(named:) load the scene from the app bundle.
  • The authoring project holds the behavior. world.tm_entity contains an owl_model entity with a tm_re_animation_graph_component. Owl Animation Graph.tm_animation_graph defines parameters named moving and speed, the same strings that OwlConstants uses 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

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

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: AnimationGraphApp follows the <ProductName>App convention. ContentView is the template root view name. OwlConstants follows the <Domain>Constants convention for a namespace enum.
  • Constants: names describe their role in the asset contract. Entity names end in Entity or Name (worldEntity, owlName), and graph inputs end in Parameter (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 in Supporting 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, then onChange, then the entity parameter. No code reads values back from the graph.
  • Concurrency is framework-owned. RealityView creates the only async context, and the target’s default MainActor isolation 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