Providing an integrated view of your timeline when playing HLS interstitials
At a glance
| Item | Summary |
|---|---|
| Purpose | Display point- and fill-occupancy HLS interstitials on one integrated playback timeline. |
| App architecture | SwiftUI menu and player views use observable view models; PlayerState owns AVFoundation playback and timeline observation. |
| Main patterns | MVVM, observable state, weak delegates, shared SharePlay/presentation coordinators. |
| Project style | Feature folders for Menu, IntegratedTimeline, SharePlay, and Helpers; models, view models, and views are separated under Menu. |
Project structure
HLSInterstitialDemo/HLSInterstitialDemo/
├── HLSInterstitialDemoApp.swift
├── Menu/
│ ├── Models/Menu.swift
│ ├── ViewModels/
│ └── Views/
├── IntegratedTimeline/
│ ├── Controls/
│ ├── Occupancy/
│ └── IntegratedTimelinePlayerView.swift
├── PlayerState.swift
├── SharePlay/
└── Helpers/
The folders follow features first, then roles within the larger Menu feature. PlayerState.swift sits at the app-feature root because both the custom player UI and SharePlay coordination use it.
Overall architecture
flowchart LR
App["HLSInterstitialDemoApp"] --> MenuUI["MenuView"]
MenuUI --> MenuVM["Menu view-model tree"]
MenuVM --> ItemVM["MenuItemViewModel"]
ItemVM --> PlayerState
PlayerState --> AVPlayer
PlayerState --> Timeline["Integrated timeline"]
PlayerState --> SharePlay["SharePlayCoordinator"]
PlayerUI["IntegratedTimelinePlayerView"] --> PlayerState
Reference code
HLSInterstitialDemo/HLSInterstitialDemo/PlayerState.swift:64 — PlayerState builds and retains the playback components (abridged).
private lazy var playerItem: AVPlayerItem = {
let asset = AVURLAsset(url: menuItem.url)
let playerItem = AVPlayerItem(asset: asset)
return playerItem
}()
lazy var player: AVPlayer = {
let player = AVPlayer(playerItem: self.playerItem)
player.playbackCoordinator.delegate = playbackCoordinatorDelegate
return player
}()PlayerState is an observable presentation model and the AVFoundation boundary. This is directly observed. Calling the structure MVVM is an interpretation supported by the source’s ViewModels folders and view-model names.
Ownership and state
classDiagram
HLSInterstitialDemoApp *-- MenuViewModel
MenuViewModel *-- MenuSectionViewModel
MenuSectionViewModel *-- MenuItemViewModel
MenuItemViewModel *-- PlayerState : while presented
PlayerState *-- AVPlayer
PlayerState *-- AVPlayerInterstitialEventController
PlayerState --> SharePlayCoordinator : shared
Ownership evidence
HLSInterstitialDemo/HLSInterstitialDemo/Menu/ViewModels/MenuItemViewModel.swift:65 — the selected item creates, retains, and explicitly invalidates its player state (abridged).
func presentPlayer(with videoMetadata: VideoMetadata) {
Task { @MainActor in
let playerState = PlayerState(menuItem: self.item, videoMetadata: videoMetadata)
unsafeSelf.wrappedValue.currentPlayerState = playerState
let playerView = IntegratedTimelinePlayerView(playerState: playerState)
ModalPresentationCoordinator.shared.requestPresentation { playerView } onDismiss: {
playerState.invalidate()
unsafeSelf.wrappedValue.currentPlayerState = nil
}
}
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
HLSInterstitialDemoApp |
MenuViewModel |
Creates one app-lifetime model | App entry creates; view model mutates its menu state. |
MenuItemViewModel |
currentPlayerState |
Retains only while content is presented | MenuItemViewModel creates, invalidates, and releases it. |
PlayerState |
AVPlayer, interstitial controller, observations |
Lazy owned components | PlayerState schedules events and publishes timeline values. |
SharePlayCoordinator |
Current group session | Process-wide shared actor | The actor serializes its session state. |
Class and protocol design
classDiagram
class MenuItemViewModelDelegate {
<<protocol>>
menuItemViewModelWantsToBePlayed()
}
MenuSectionViewModel ..|> MenuItemViewModelDelegate
MenuItemViewModel --> MenuItemViewModelDelegate : weak delegate
PlayerCoordinatorDelegate ..|> AVPlayerPlaybackCoordinatorDelegate
Reference code
HLSInterstitialDemo/HLSInterstitialDemo/Menu/ViewModels/MenuItemViewModel.swift:12 — child-to-parent intent is a small class-only protocol.
protocol MenuItemViewModelDelegate: AnyObject {
func menuItemViewModelWantsToBePlayed(_ itemViewModel: MenuItemViewModel)
}
weak var delegate: MenuItemViewModelDelegate?The delegate chain keeps selection intent out of SwiftUI views and avoids retaining the parent view-model node.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
MenuItemViewModel.currentPlayerState |
private |
Only the item view model can replace or release playback state. | Protects the presentation-lifetime invariant. |
PlayerState.playerItem |
private lazy |
Views cannot replace the item behind the exposed player. | Keeps asset/event setup inside the playback owner. |
ModalPresentationCoordinator.presentation |
fileprivate |
Same-file view-modifier extensions can bind it; other files cannot. | Deliberately supports split conformances in one file. |
PlayerState observable values |
public members on an internal class |
Accessible within the app target; the internal containing type prevents a public module API. | Likely emphasizes view readability, not library API design. |
HLSInterstitialDemo/HLSInterstitialDemo/Helpers/ModalPresentationCoordinator.swift:18:
@Observable
class ModalPresentationCoordinator {
static var shared = ModalPresentationCoordinator()
fileprivate var presentation: ModalPresentation?
}Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| JSON menu decoding and view-model tree | MenuViewModel |
Converts static configuration into observable presentation nodes. |
| Interstitial creation, scheduling, and observation | PlayerState |
Co-locates AVFoundation invariants with the player it owns. |
| Timeline drawing and transport controls | IntegratedTimeline/ views |
SwiftUI types render state and send simple playback intents. |
| Group-session lifecycle | SharePlayCoordinator |
An actor isolates process-wide SharePlay state. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| MVVM | HLSInterstitialDemo/HLSInterstitialDemo/Menu/ViewModels/MenuViewModel.swift:14 and HLSInterstitialDemo/HLSInterstitialDemo/Menu/Views/MenuView.swift:10 |
Keeps menu decoding/selection outside rendering; pattern name is an inference from explicit roles. |
| Observable state | HLSInterstitialDemo/HLSInterstitialDemo/PlayerState.swift:13 |
Lets multiple timeline controls read one playback state owner. |
| Delegate | HLSInterstitialDemo/HLSInterstitialDemo/Menu/ViewModels/MenuItemViewModel.swift:12 |
Routes child intent upward without a hard parent type dependency. |
| Shared coordinator | HLSInterstitialDemo/HLSInterstitialDemo/SharePlay/SharePlayCoordinator.swift:12 |
Gives menu and player code one group-session authority; introduces global lifetime. |
Naming conventions
- Types use role suffixes:
View,ViewModel,Coordinator,Delegate, andState. - Methods use commands for intent (
presentPlayer,dismissActiveContent,scheduleInterstitialEvents) andmakefor construction. - Boolean names use
is/should; private sections are grouped withMARKcomments. - Extensions use
Type+Additions.swift; feature view files generally match their primary type.
Architecture takeaways
- One
PlayerStatecentralizes AVPlayer, interstitial scheduling, timeline snapshots, and observation lifetimes. - Menu view models form an ownership tree; weak delegate links carry intent back upward.
- Shared coordinators are reserved for genuinely cross-feature lifecycles: SharePlay and modal presentation.
- The custom timeline UI reads state but does not own AVFoundation configuration.
Source map
| Source file | Relevant symbols |
|---|---|
HLSInterstitialDemo/HLSInterstitialDemo/HLSInterstitialDemoApp.swift |
HLSInterstitialDemoApp |
HLSInterstitialDemo/HLSInterstitialDemo/Menu/ViewModels/MenuViewModel.swift |
MenuViewModel |
HLSInterstitialDemo/HLSInterstitialDemo/Menu/ViewModels/MenuItemViewModel.swift |
MenuItemViewModel, delegate |
HLSInterstitialDemo/HLSInterstitialDemo/PlayerState.swift |
PlayerState |
HLSInterstitialDemo/HLSInterstitialDemo/SharePlay/SharePlayCoordinator.swift |
SharePlayCoordinator |