Sample CodevisionOSReviewed 2026-07-21View on Apple Developer

Creating a multiview video playback experience in visionOS

At a glance

Item Summary
Purpose Selects several videos and coordinates embedded, expanded, and multiview playback experiences.
App architecture A main-actor observable state model owns per-video models/controllers and reduces AVExperienceController transitions into SwiftUI state.
Main patterns State model, per-item model, delegate reducer, grouped transitions, representable/hosting-controller adapters.
Project style Single visionOS target organized into Models, Views, and Helpers.

Project structure

MultiviewVideoPlayback/
├── MultiviewVideoPlaybackApp.swift
├── Models/
│   ├── MultiviewStateModel.swift
│   ├── VideoModel.swift
│   └── Video.swift
├── Views/
│   ├── ContentView.swift
│   ├── VideoHomeView.swift
│   ├── MultiviewVideoSelectionView.swift
│   └── ItemVideoPlayer.swift
└── Helpers/
    ├── SceneDelegate.swift
    └── AVMultiviewManager+Extension.swift

Structure observations

  • Models own transition/playback state; views send selection intents and render observed values.
  • Helpers bridge SwiftUI content and scene lifecycle into UIKit/AVKit APIs.

Overall architecture

Reference code

MultiviewVideoPlayback/Models/MultiviewStateModel.swift:47 — a selection intent becomes an experience transition owned by the state model.

func videoSelected(videoModel: VideoModel, inMultiview: Bool) async {
    if inMultiview {
        await AVExperienceController.withTransitionGroup { group in
            group.addTransition {
                await videoModel.viewController.experienceController.transition(
                    to: videoModel.isAddedToMultiview ? .embedded : .multiview)
            }
        }
    } else {
        // Transition the selected item to its expanded experience.
    }
}

Interpretation

MultiviewStateModel is both application state authority and AVExperienceController delegate. Each VideoModel owns the player/controller pair for one immutable Video; views do not manipulate controllers directly.

Ownership and state

Ownership evidence

MultiviewVideoPlayback/Models/VideoModel.swift:10 — one model creates and retains all playback objects for one video.

@Observable @MainActor
class VideoModel {
    let video: Video
    private let player: AVPlayer
    let viewController: AVPlayerViewController
    var isAddedToMultiview = false

    init(video: Video) {
        let playerController = AVPlayerViewController()
        self.video = video
        self.viewController = playerController
        self.player = AVPlayer(playerItem: video.playerItem)
        self.viewController.player = player
    }
}
Owner Object or state Relationship Mutation authority
ContentView MultiviewStateModel Creates through @State State model
State model Video-model array, embedded selection, scene, loading state Creates and retains State-model methods/delegate callbacks
VideoModel Player, player VC, membership flag Per-item ownership VideoModel playback methods; state model membership updates
Scene delegate Current scene reference Observes UIKit lifecycle Delegate callbacks

Class and protocol design

Type Responsibility Depends on
MultiviewStateModel Populate items, coordinate transitions, reduce delegate events AVExperienceController delegate API
VideoModel Encapsulate one player/controller and playback commands Video, AVKit
Video Immutable metadata and player-item factory URL and external metadata
ItemVideoPlayer Embed an existing player view controller in SwiftUI UIViewControllerRepresentable
CustomSceneDelegate Publish active scene for fallback placement UIKit scene lifecycle

Access control

Symbol Access Verified effect Likely rationale
VideoModel.player private Views/state model use high-level methods or controller, not direct player mutation. Keep cursor/play commands consistent.
State/model types implicit internal Collaborate within the app module only. No library boundary.
AVMetadataItem.metadataItem helper fileprivate Only Video.swift builds metadata objects. Localize serialization detail.
App delegate adaptor private SwiftUI app retains lifecycle glue without exposing it to views. Entry-point implementation detail.
Most view properties implicit internal SwiftUI composition can pass models directly. Sample favors clarity over a narrower internal surface.

Reference code

MultiviewVideoPlayback/Models/Video.swift:34 — metadata construction is confined to the model file.

extension AVMetadataItem {
    fileprivate static func metadataItem(
        key identifier: AVMetadataIdentifier,
        value: Any
    ) -> AVMetadataItem {
        // Build and copy a mutable metadata item.
    }
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Selection UI MultiviewVideoSelectionView Sends intents; no AVKit state mutation.
Transition coordination and state reduction MultiviewStateModel Single observable authority.
Per-video playback/reset VideoModel Same owner as player.
Metadata/player-item creation Video Immutable content model.
SwiftUI/UIKit bridging ItemVideoPlayer, AVMultiviewManager extension Framework adaptation boundary.
Fallback scene supply Scene delegate + state model UIKit lifecycle information feeds AVKit placement.

Design patterns

Pattern Source evidence Purpose or tradeoff
Observable state model MultiviewVideoPlayback/Models/MultiviewStateModel.swift:11 Gives all selection views one transition-aware source of truth.
Per-item model MultiviewVideoPlayback/Models/VideoModel.swift:10 Keeps each player/controller lifecycle together.
Delegate reducer MultiviewVideoPlayback/Models/MultiviewStateModel.swift:87 Converts transition callbacks into membership/embedded state.
Transition group MultiviewVideoPlayback/Models/MultiviewStateModel.swift:53 Coordinates AVKit experience changes as one operation.
Adapter MultiviewVideoPlayback/Views/ItemVideoPlayer.swift:12 Embeds a UIKit controller in SwiftUI.

Main application flow

Reference code

MultiviewVideoPlayback/Views/MultiviewVideoSelectionView.swift:24 — the view forwards a typed intent and awaits the model.

ForEach(multiviewStateModel.videoModels, id: \.video) { videoModel in
    Button {
        Task {
            await multiviewStateModel.videoSelected(
                videoModel: videoModel,
                inMultiview: fromMultiviewContentSelection)
        }
    } label: { VideoItemSelectionView(videoModel: videoModel, inMultiviewContentSelection: fromMultiviewContentSelection) }
}

Naming conventions

  • StateModel names the shared reducer; VideoModel names per-item playback state; Video is immutable content.
  • Experience methods use event/action language: videoSelected, prepareForTransition, didChangeTransitionContext.
  • Boolean names communicate membership/capability: isAddedToMultiview, supportsEmbeddedPlaybackExperience.
  • Helper extension filename names the extended framework type.

Architecture takeaways

  • Keep controller/player lifetime in per-item models and aggregate transition truth in one state model.
  • Treat AVExperienceController delegate callbacks as the authoritative transition result, not the button tap.
  • Use transition groups when several experience changes must be coordinated.
  • Isolate SwiftUI/UIKit seams in representables and hosting-controller helpers.

Source map

Source file Relevant symbols
MultiviewVideoPlayback/Views/ContentView.swift State-model ownership and scene handoff
MultiviewVideoPlayback/Models/MultiviewStateModel.swift Transition orchestration and delegate reduction
MultiviewVideoPlayback/Models/VideoModel.swift Per-video player/controller ownership
MultiviewVideoPlayback/Models/Video.swift Content and metadata
MultiviewVideoPlayback/Views/MultiviewVideoSelectionView.swift Selection intents
MultiviewVideoPlayback/Helpers/AVMultiviewManager+Extension.swift SwiftUI content-selection adapter