Sample CodevisionOSReviewed 2026-07-21View on Apple Developer

Playing immersive media with AVKit

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

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

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

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