Sample CodeiOS 18.0+, iPadOS 18.0+, Mac Catalyst 18.0+Reviewed 2026-07-21View on Apple Developer

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

Reference code

HLSInterstitialDemo/HLSInterstitialDemo/PlayerState.swift:64PlayerState 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

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

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, and State.
  • Methods use commands for intent (presentPlayer, dismissActiveContent, scheduleInterstitialEvents) and make for construction.
  • Boolean names use is/should; private sections are grouped with MARK comments.
  • Extensions use Type+Additions.swift; feature view files generally match their primary type.

Architecture takeaways

  • One PlayerState centralizes 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