Creating a seamless multiview playback experience
At a glance
| Item |
Summary |
| Purpose |
Present several streams, coordinate playback, prioritize network use, and nominate one focus player for audio and external playback. |
| App architecture |
SwiftUI MVVM with a menu view model, a layout-level player coordinator, and one observable state object per AVPlayer. |
| Main patterns |
MVVM, observable state, composition root, coordinated collaborators, and a SwiftUI-to-AVKit adapter. |
| Project style |
Feature roles are separated into Models, ViewModels, Views, and Utilities/Extensions. |
Project structure
MultiviewPlaybackSampleApp/
├── MultiviewPlaybackSampleAppApp.swift # Composition root
├── Menu.json # Stream configuration
├── Models/
│ ├── Menu.swift # Codable menu values
│ └── PlayerState.swift # One player and its state
├── ViewModels/
│ ├── MainViewModel.swift # Menu/navigation state
│ └── LayoutGridViewModel.swift # Multi-player coordination
├── Views/
│ ├── MainView.swift
│ ├── GridView.swift / LayoutView.swift
│ ├── LayoutGridView.swift / PlayerView.swift
│ └── PlayerViewController.swift # AVKit bridge
└── Utilities/ # Decode, logging, URL, and style helpers
Structure observations
- Folder names express architectural role; feature-specific names connect views to their view models.
LayoutGridViewModel is the aggregate owner for a screenful of players, while PlayerState owns one player’s framework behavior.
- JSON decoding and framework-value conversion live in small extensions rather than either view model.
Overall architecture
flowchart LR
App["App"] --> MainVM["MainViewModel"]
App --> MainView["MainView"]
MainView --> LayoutVM["LayoutGridViewModel"]
LayoutVM --> States["PlayerState × N"]
States --> Players["AVPlayer × N"]
LayoutVM --> Medium["AVPlaybackCoordinationMedium"]
States --> Arbiter["AVRoutingPlaybackArbiter.shared"]
MainView --> AVKit["AVPlayerViewController adapter"]
Reference code
MultiviewPlaybackSampleApp/MultiviewPlaybackSampleAppApp.swift:11 and MultiviewPlaybackSampleApp/Views/MainView.swift:31 — two-level composition
let mainViewModel = MainViewModel.createViewModelWithMenu
MainView(viewModel: mainViewModel)
// When a playback mode is selected:
layoutGridViewModel = LayoutGridViewModel(selectedItem: viewModel.menuItem)
viewModel.showGridView()
Interpretation
The app creates long-lived menu state, while MainView creates a fresh layout coordinator for each playback destination. That coordinator creates and manages the player collection. PlayerState translates layout-level intent into AVFoundation and AVRouting operations.
Ownership and state
classDiagram
MultiviewPlaybackSampleAppApp *-- MainViewModel : creates
MainView *-- LayoutGridViewModel : creates as State
LayoutGridViewModel *-- PlayerState : creates many
LayoutGridViewModel *-- AVPlaybackCoordinationMedium : creates
PlayerState *-- AVPlayer : lazy creates
PlayerState o-- AVRoutingPlaybackArbiter : shared
PlayerViewController --> PlayerState : receives
Ownership evidence
MultiviewPlaybackSampleApp/ViewModels/LayoutGridViewModel.swift:53 — layout-owned players
func createAllPlayers() {
resetValues()
for (index, asset) in selectedItem.assets.enumerated() {
let playerState = PlayerState(/* asset values */)
playerState.connectToAVFCoordinationMedium(
coordinationMedium: coordinationMedium
)
playersToShow.append(playerState)
}
}
| Owner |
Object or state |
Relationship |
Mutation authority |
| App root |
MainViewModel |
Creates once and injects |
MainViewModel and bound view actions |
MainView |
Optional LayoutGridViewModel |
Creates as @State, resets on return |
MainView replaces; layout model mutates internals |
LayoutGridViewModel |
Player array, grid, focus, fullscreen, coordination medium |
Creates and coordinates |
Layout model methods in observed practice |
PlayerState |
AVPlayer, KVO tokens, focus/playback flags |
Lazy-creates and observes |
PlayerState methods; properties remain module-settable |
| AVFoundation |
Playback coordinator relation |
Connects each player to shared medium |
PlayerState.connect... and invalidate() |
Class and protocol design
MultiviewPlaybackSampleApp/Models/Menu.swift:11 — protocol composition for data models
typealias DataModel = (Codable & Equatable & Sendable)
struct MenuItem: DataModel, Identifiable { /* immutable fields */ }
struct MenuAsset: DataModel, Identifiable, Hashable { /* ... */ }
| Type |
Responsibility |
Depends on |
MainViewModel |
Decode menu, derive grid limits, and drive navigation |
Menu values, SwiftUI navigation |
LayoutGridViewModel |
Create/remove players and coordinate focus, grid, and fullscreen state |
PlayerState, AVPlaybackCoordinationMedium |
PlayerState |
Own one player, route focus, network priority, observation, and cleanup |
AVFoundation, AVRouting |
PlayerViewController |
Adapt AVPlayerViewController into SwiftUI |
AVKit, PlayerState |
DataModel composition |
Require menu values to be codable, comparable, and sendable |
Standard library protocols |
There is protocol composition for value models, but no app-defined playback protocol. The player collaborators are concrete.
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
PlayerState.queue |
private |
Inaccessible outside PlayerState. |
Reserves implementation-only scheduling state, although this source does not otherwise use the queue. |
| Menu fields |
internal immutable let |
Readable within the target but not replaceable. |
Configuration is value data loaded once. |
| View-model and player observable properties |
internal by default and mostly mutable |
Any code in the app target can mutate them. |
Keeps the sample concise, but ownership conventions are not compiler-enforced. |
PlayerViewController.init |
declared public, effectively capped by internal enclosing type |
Still not an exported reusable API. |
Wider than needed; likely incidental sample style. |
| App types |
internal by default |
No architecture surface is exported outside the app module. |
Appropriate for an application target. |
Reference code
MultiviewPlaybackSampleApp/Models/PlayerState.swift:17 — broad observable surface with one private detail
let assetURL: URL
var playerRate: Float = 0
var isFocusPlayer = false
var isFullScreen = false
private let queue = DispatchQueue(label: "com.apple...PlayerState.queue")
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Menu loading and route selection |
MainViewModel |
Top-level feature state |
| Grid geometry, player creation, focus, and fullscreen |
LayoutGridViewModel |
Operations span the complete player collection |
| Playback, seeking, muting, route preference, KVO cleanup |
PlayerState |
Operations concern exactly one AVPlayer lifecycle |
| AVKit embedding |
PlayerViewController |
Platform-view adaptation kept out of domain state |
| Network-priority mapping |
NetworkPriority extension |
Translation stays beside the source enum |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| MVVM |
MultiviewPlaybackSampleApp/Views/MainView.swift:12, MultiviewPlaybackSampleApp/Views/LayoutGridView.swift:12 |
Views bind to observable coordinators instead of directly managing player graphs. |
| Aggregate coordinator |
MultiviewPlaybackSampleApp/ViewModels/LayoutGridViewModel.swift:54 |
One owner applies collection-wide focus and lifecycle rules. |
| Per-resource state object |
MultiviewPlaybackSampleApp/Models/PlayerState.swift:16 |
Encapsulates one player’s AVFoundation and AVRouting behavior. |
| Adapter |
MultiviewPlaybackSampleApp/Views/PlayerViewController.swift:13 |
Bridges UIKit/AVKit playback into SwiftUI. |
| Protocol composition |
MultiviewPlaybackSampleApp/Models/Menu.swift:11 |
Applies consistent data contracts without defining an empty marker protocol. |
Naming conventions
- Role suffixes are consistent:
View, ViewModel, State, and ViewController.
- Collection-level methods name their scope:
createAllPlayers, setFocusOnPlayer, and setFullScreenPlayer.
- Boolean properties use
is/should: isFocusPlayer, isFullScreen, shouldBeHidden.
- Extension files use
Type+Additions.swift; view files match their primary type.
Architecture takeaways
- Model a multi-player screen with one collection coordinator and one state owner per player.
- Share a playback coordination medium while keeping player cleanup local.
- A view adapter is an effective boundary between declarative SwiftUI and
AVPlayerViewController.
- If this grew beyond a sample,
private(set) and playback protocols would enforce the ownership already implied by call sites.
Source map
| Source file |
Relevant symbols |
MultiviewPlaybackSampleApp/MultiviewPlaybackSampleAppApp.swift |
app composition root |
MultiviewPlaybackSampleApp/ViewModels/MainViewModel.swift |
MainViewModel, MenuNavigationRoute |
MultiviewPlaybackSampleApp/ViewModels/LayoutGridViewModel.swift |
LayoutGridViewModel |
MultiviewPlaybackSampleApp/Models/PlayerState.swift |
PlayerState |
MultiviewPlaybackSampleApp/Models/Menu.swift |
menu values, DataModel, NetworkPriority |
MultiviewPlaybackSampleApp/Views/PlayerViewController.swift |
AVKit bridge |