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
flowchart LR
App["SwiftUI App"] --> View["ContentView / selection views"]
View --> State["MultiviewStateModel @MainActor"]
State --> VideoModels["VideoModel per video"]
VideoModels --> PVC["AVPlayerViewController"]
PVC --> Experience["AVExperienceController"]
State -. delegate .-> Experience
State --> Group["withTransitionGroup"]
Scene["CustomSceneDelegate"] --> State
Select["AVMultiviewManager content selection"] --> View
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
classDiagram
ContentView *-- MultiviewStateModel : State owns
MultiviewStateModel *-- VideoModel : array owns
VideoModel *-- AVPlayer : private owns
VideoModel *-- AVPlayerViewController : owns
AVPlayerViewController *-- AVExperienceController : framework owns
MultiviewStateModel o-- UIScene : observes reference
CustomSceneDelegate o-- UIScene : observes lifecycle
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
sequenceDiagram
actor User
participant View as Selection view
participant State as State model
participant Exp as Experience controller
User->>View: Add/remove video
View->>State: videoSelected(item, inMultiview)
State->>Exp: grouped transition
Exp-->>State: transition context delegate callback
State->>State: update isAddedToMultiview/embeddedVideo
State-->>View: observable state rerenders
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 |