At a glance
| Item |
Summary |
| Purpose |
Presents spatial and immersive videos through AVKit’s system playback experiences. |
| App architecture |
The app owns and environment-injects a main-actor player model that creates AVPlayerViewController and coordinates experience transitions. |
| Main patterns |
Environment-injected façade, AVExperienceController delegate, scene-lifecycle adapter, immutable video catalog. |
| Project style |
Small single visionOS target with one model, one scene helper, and composable catalog views. |
Project structure
AVKitImmersivePlayback/
├── AVKitImmersivePlaybackApp.swift
├── ContentView.swift
├── VideoCard.swift
├── PlayerModel.swift
├── SceneProvider.swift
└── Video.swift
Structure observations
- The root app, not a leaf view, owns playback state and injects it through SwiftUI environment.
- Content metadata is an immutable value catalog; all mutable framework state stays in
PlayerModel.
Overall architecture
flowchart LR
App["AVKitImmersivePlaybackApp"] --> Model["PlayerModel @MainActor"]
App --> Env["SwiftUI environment"]
Env --> Card["VideoCard"]
Card --> Model
Catalog["Video.library"] --> Card
Model --> Player["AVPlayer"]
Model --> PVC["AVPlayerViewController"]
PVC --> Experience["AVExperienceController"]
Model -. delegate .-> Experience
Scene["SceneProvider"] --> Model
Reference code
AVKitImmersivePlayback/PlayerModel.swift:33 — the façade turns one view intent into load, UI configuration, and an asynchronous experience transition.
func playVideo(_ video: Video) {
loadVideo(video)
configurePlaybackUI()
Task { await presentPlayer() }
}
private func configurePlaybackUI() {
let controller = AVPlayerViewController()
controller.experienceController.allowedExperiences = .recommended()
controller.experienceController.delegate = self
controller.player = player
playerViewController = controller
}
Interpretation
Views know only a Video and playVideo command. The model owns AVPlayer, constructs the transient system UI, supplies scene placement, and responds to experience availability/lifecycle.
Ownership and state
classDiagram
AVKitImmersivePlaybackApp *-- PlayerModel : State owns
PlayerModel *-- AVPlayer : owns
PlayerModel *-- AVPlayerViewController : owns while presented
AVPlayerViewController *-- AVExperienceController : framework owns
PlayerModel o-- UIScene : observes
SceneProvider o-- UIScene : publishes
ContentView --> PlayerModel : environment reads
VideoCard --> PlayerModel : sends intent
Ownership evidence
AVKitImmersivePlayback/AVKitImmersivePlaybackApp.swift:10 — the root retains and injects one player model for the window.
@main
struct AVKitImmersivePlaybackApp: App {
@State private var player = PlayerModel()
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
var body: some Scene {
WindowGroup { ContentView().environment(player) }
}
}
| Owner |
Object or state |
Relationship |
Mutation authority |
| Root app |
PlayerModel, app delegate adaptor |
Creates and retains |
Player model for playback |
PlayerModel |
Player, current player VC, scene |
Creates/retains |
Model methods/delegate callbacks |
SceneProvider |
Current scene reference |
Observes UIKit lifecycle |
Scene delegate methods |
Video.library |
Catalog values |
Static immutable data |
Source declaration only |
Class and protocol design
| Type |
Responsibility |
Depends on |
PlayerModel |
Load metadata, configure audio/UI, present/reset experiences |
AVPlayer, AVPlayerViewController |
AVExperienceController.Delegate conformance |
Supply placement and react to experience availability |
Scene and transition contexts |
SceneProvider |
Publish active/disconnected scene |
UIWindowSceneDelegate |
Video |
Hold URL/localized metadata/artwork |
Value semantics |
VideoCard |
Render one catalog item and send play intent |
Environment PlayerModel |
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
PlayerModel.player and playerViewController |
private |
Views cannot replace media or presentation controller directly. |
Preserve load/present/reset lifecycle. |
| Load/configure/present/metadata/audio/reset helpers |
private |
playVideo is the single view-facing operation. |
Keep façade small. |
| Environment model references |
private |
Only each view implementation uses the injected model. |
Avoid exposing view internals. |
| Root model/app delegate wrappers |
private |
Ownership remains an app-entry implementation detail. |
Prevent accidental external mutation. |
| Types |
implicit internal |
App-only module. |
No reusable framework API. |
Reference code
AVKitImmersivePlayback/PlayerModel.swift:11 — a small internal command surface wraps private framework state.
@MainActor
@Observable
class PlayerModel {
var scene: UIScene?
private let player = AVPlayer()
private var playerViewController: AVPlayerViewController?
}
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Catalog rendering |
ContentView, VideoCard |
Pure presentation and intent. |
| Media load and metadata mapping |
PlayerModel |
Coupled to player-item lifecycle. |
| Playback UI and transition |
PlayerModel |
One framework façade. |
| Audio session configuration |
PlayerModel |
Playback service setup. |
| Placement/experience updates |
Player-model delegate extension |
Framework callback boundary. |
| Scene acquisition |
SceneProvider |
UIKit lifecycle adapter. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Environment-injected model |
AVKitImmersivePlayback/AVKitImmersivePlaybackApp.swift:13 |
Shares one playback authority without prop drilling. |
| Façade |
AVKitImmersivePlayback/PlayerModel.swift:33 |
Reduces AVKit setup to playVideo. |
| Delegate |
AVKitImmersivePlayback/PlayerModel.swift:135 |
Supplies placement and adapts allowed experiences over time. |
| Scene adapter |
AVKitImmersivePlayback/SceneProvider.swift:10 |
Bridges UIKit scene state into observable app state. |
| Value catalog |
AVKitImmersivePlayback/Video.swift:28 |
Keeps immutable content separate from mutable playback. |
Main application flow
sequenceDiagram
actor User
participant Card as VideoCard
participant Model as PlayerModel
participant Exp as ExperienceController
User->>Card: Select video
Card->>Model: playVideo(video)
Model->>Model: create item metadata and player VC
Model->>Exp: transition(to: expanded)
Exp-->>Model: completed or reversed
alt completed
Model->>Model: player.play()
end
Reference code
AVKitImmersivePlayback/PlayerModel.swift:67 — playback begins only after AVKit completes the expanded transition.
private func presentPlayer() async {
switch await playerViewController?.experienceController.transition(to: .expanded) {
case .completed: player.play()
case .reversed(reason: let reason): print("Unable to start playback: \(reason).")
default: fatalError()
}
}
Naming conventions
PlayerModel names observable playback state; SceneProvider names lifecycle data supply.
- Commands use user-domain verbs:
playVideo, loadVideo, presentPlayer.
- Helpers use
configure/create prefixes for setup and transformation.
VideoCard and Video.library separate presentation from catalog data.
Architecture takeaways
- Own and inject one playback model at the app boundary when playback spans many views.
- Keep AVPlayerViewController transient and reset it when returning to embedded state.
- Start media only after the requested AVKit experience transition completes.
- Model scene discovery as an adapter rather than coupling SwiftUI views to UIKit lifecycle callbacks.
Source map
| Source file |
Relevant symbols |
AVKitImmersivePlayback/AVKitImmersivePlaybackApp.swift |
Root ownership and environment injection |
AVKitImmersivePlayback/PlayerModel.swift |
Playback façade and experience delegate |
AVKitImmersivePlayback/SceneProvider.swift |
Scene lifecycle bridge |
AVKitImmersivePlayback/Video.swift |
Immutable catalog model |
AVKitImmersivePlayback/VideoCard.swift |
Play intent |