Working with Overlays and Parental Controls in tvOS
At a glance
| Item |
Summary |
| Purpose |
Adds interactive overlays, playback-restriction checks, and live-channel navigation to tvOS playback. |
| App architecture |
The menu owns a reusable playback screen; that screen owns and replaces the AVKit controller, player, overlay, pending item, and observations for each playback request. |
| Main patterns |
Screen-owned coordinator, framework delegate, KVO observer, metadata-driven policy, and child-view-controller composition. |
| Project style |
A compact storyboard/XIB UIKit app with one menu controller, one playback controller, and one overlay controller. |
Project structure
TVAVPlayerViewControllerDemo/
└── TVAVPlayerViewControllerDemo/
├── AppDelegate.swift
├── MenuViewController.swift
├── VideoPlaybackViewController.swift
├── CustomInteractiveVideoOverlayViewController.swift
└── Base.lproj/
├── Main.storyboard
└── CustomInteractiveVideoOverlay.xib
Structure observations
- Screen controllers map directly to the menu, playback, and interactive-overlay responsibilities.
- The overlay’s visual hierarchy lives in a XIB; playback navigation lives in the main storyboard.
- All player lifecycle and policy logic stays in the playback controller rather than the menu or overlay.
Overall architecture
flowchart LR
Menu["MenuViewController"] --> Playback["VideoPlaybackViewController"]
Menu --> Metadata["URL, title, description, rating"]
Metadata --> Playback
Playback --> PVC["AVPlayerViewController"]
Playback --> Player["AVQueuePlayer"]
Playback --> Pending["Pending AVPlayerItem"]
Playback --> Overlay["CustomInteractiveVideoOverlay"]
PVC --> Overlay
Playback -. delegate .-> PVC
Pending --> Policy["Playback restrictions authorization"]
Reference code
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift:100 — the playback screen replaces the AVKit controller and attaches its owned overlay before playback begins.
private func loadNewPlayerViewController() {
let newPlayerViewController = AVPlayerViewController()
newPlayerViewController.delegate = self
newPlayerViewController.customOverlayViewController = customInteractiveVideoOverlay
if let oldPlayerViewController = self.playerViewController {
oldPlayerViewController.removeFromParent()
oldPlayerViewController.viewIfLoaded?.removeFromSuperview()
}
self.playerViewController = newPlayerViewController
player = AVQueuePlayer()
addChild(newPlayerViewController)
}
Interpretation
The menu supplies content facts and presents one playback owner. The playback controller turns those facts into AV metadata, gates the pending item through parental authorization, and composes the AVKit UI with a custom overlay.
Ownership and state
classDiagram
MenuViewController *-- VideoPlaybackViewController : creates in both initializers
VideoPlaybackViewController *-- CustomInteractiveVideoOverlay : owns
VideoPlaybackViewController *-- AVPlayerViewController : owns current child
VideoPlaybackViewController *-- AVQueuePlayer : owns current player
VideoPlaybackViewController *-- NSKeyValueObservation : owns tokens
VideoPlaybackViewController *-- AVPlayerItem : holds pending item
AVQueuePlayer o-- AVPlayerItem : current item
AVPlayerViewController o-- AVQueuePlayer : receives after authorization
AVPlayerViewController o-- CustomInteractiveVideoOverlay : presents
Ownership evidence
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift:18 — one controller retains all mutable playback collaborators and policy-observation state.
@objc dynamic var playerViewController: AVPlayerViewController?
let customInteractiveVideoOverlay = CustomInteractiveVideoOverlay(
nibName: "CustomInteractiveVideoOverlay", bundle: nil)
var player: AVQueuePlayer?
@objc dynamic var pendingPlayerItem: AVPlayerItem?
var playerItemStatusObservation: NSKeyValueObservation?
var currentItemErrorObservation: NSKeyValueObservation?
var isPlaybackActive = false
| Owner |
Object or state |
Relationship |
Mutation authority |
MenuViewController |
VideoPlaybackViewController |
Creates in either initializer and retains |
Menu presents and loads content |
| Playback controller |
AVKit controller and queue player |
Recreates for each load and tears down on dismissal |
Playback controller only in practice |
| Playback controller |
Overlay |
Creates once and assigns to each new AVKit controller |
Playback controller |
| Playback controller |
Pending item and KVO tokens |
Retains during readiness/policy gate |
KVO callbacks clear or dismiss |
| AVKit controller |
Player and overlay references |
Receives after setup/authorization |
Playback owner and AVKit lifecycle |
Class and protocol design
| Type or protocol |
Responsibility |
Depends on |
MenuViewController |
Convert poster selection into content metadata and present playback |
TVUIKit poster views and playback controller |
VideoPlaybackViewController |
Build metadata, gate playback, compose AVKit UI, handle errors and dismissal |
AVKit, KVO, UIKit containment |
AVPlayerViewControllerDelegate conformance |
Approve dismissal and implement next/previous channel requests |
AVPlayerViewController |
CustomInteractiveVideoOverlay |
Supply focusable custom controls over video |
XIB outlets and tvOS focus engine |
There is no app-defined protocol layer. The sample uses AVKit’s delegate directly because one playback controller is both the framework adapter and lifecycle owner.
Access control
| Symbol |
Access |
Verified effect |
Likely rationale |
Metadata factories, player-controller loader, and _dismiss |
private |
Only the playback implementation can build policy metadata or alter containment directly. |
Preserve the load/authorize/play/dismiss sequence. |
| Controllers and stored playback state |
implicit internal |
Other files in the app target can refer to them; nothing is exported as public. |
The sample is an application, not a reusable framework. |
playerViewController and pendingPlayerItem |
implicit internal, plus @objc dynamic |
Module code can access them; Objective-C dynamic dispatch enables key-path observation. |
KVO requires runtime-visible properties; dynamic does not itself make them public. |
checkParentalControlsExplicitly |
implicit internal global constant |
Any source file in the target can read the sample switch. |
Makes automatic versus explicit behavior easy to compare. |
| Overlay outlet |
implicit internal |
Storyboard/XIB wiring and app code may access it. |
Simple target-local UI integration. |
Reference code
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift:88 — metadata and controller-construction primitives are private; callers receive the higher-level loadAndPlay command.
private func metadataItem(_ identifier: AVMetadataIdentifier,
stringValue value: String) -> AVMutableMetadataItem {
let metadataItem = AVMutableMetadataItem()
metadataItem.value = value as NSString
metadataItem.identifier = identifier
return metadataItem
}
private func loadNewPlayerViewController() {
let newPlayerViewController = AVPlayerViewController()
// setup remains inside the playback owner
}
Logic ownership and placement
| Logic |
Owning type or file |
Placement rationale |
| Poster-to-content mapping |
MenuViewController |
Menu presentation knows which catalog choice the user selected. |
| AV metadata creation, including content rating |
Playback controller |
Metadata directly drives system playback and restriction behavior. |
| Player/controller replacement and child containment |
Playback controller |
One owner maintains the UIKit and AVKit lifecycle. |
| Readiness, error, and authorization handling |
commonInit KVO closures |
These transitions originate from observed player state. |
| Focus order for custom controls |
Overlay controller |
Focus is a view-level concern. |
| Channel skip and dismissal integration |
AVKit delegate methods |
Framework-requested navigation belongs at the delegate boundary. |
Design patterns
| Pattern |
Source evidence |
Purpose or tradeoff |
| Screen-owned coordinator |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/MenuViewController.swift:13 |
Reuses one playback authority across menu selections. |
| Framework delegate |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift:166 |
Adapts AVKit dismissal and channel requests to app behavior. |
| KVO observer |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift:31 |
Delays policy and playback decisions until item readiness; requires careful token ownership. |
| Metadata-driven policy |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift:116 |
Places content rating alongside title/description on the item AVKit evaluates. |
| Child controller composition |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift:100 |
Embeds system playback UI while retaining an app-owned presentation shell. |
Main application flow
sequenceDiagram
actor User
participant Menu
participant Playback as Playback controller
participant Item as Pending player item
participant Policy as Restriction authorization
participant AVKit as AVPlayerViewController
User->>Menu: Select poster
Menu->>Playback: loadAndPlay metadata
Playback->>Item: create item and external metadata
Item-->>Playback: status readyToPlay via KVO
Playback->>Policy: request authorization
alt authorized
Playback->>AVKit: attach queue player
Playback->>AVKit: start playback
else denied or failed
Playback->>Playback: dismiss
end
Reference code
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift:31 — the observer attaches the player to AVKit only after the pending item is ready and authorization succeeds.
playerItemStatusObservation = observe(\.pendingPlayerItem?.status) { object, _ in
DispatchQueue.main.async {
if object.pendingPlayerItem?.status == .readyToPlay {
object.pendingPlayerItem?.requestPlaybackRestrictionsAuthorization { success, _ in
if success {
object.pendingPlayerItem = nil
object.playerViewController?.player = object.player
object.player?.rate = 1.0
} else {
_ = object._dismiss()
}
}
}
}
}
Naming conventions
- Screen types use descriptive controller names:
MenuViewController, VideoPlaybackViewController, and CustomInteractiveVideoOverlay.
- Commands state intent:
presentVideo, loadAndPlay, and loadNewPlayerViewController.
- Observation tokens name the key path they protect:
playerItemStatusObservation and currentItemErrorObservation.
pendingPlayerItem distinguishes gated content from the player’s authorized current item.
_dismiss is a private helper whose leading underscore separates its Boolean-returning cleanup operation from the AVKit delegate callback.
Architecture takeaways
- Keep the player item pending until both readiness and policy checks succeed.
- Put rating metadata on the item before exposing the player to system playback UI.
- Let one playback owner create, embed, replace, and dismantle the AVKit controller.
- Retain KVO tokens alongside the state they observe and marshal observed mutations to the main queue.
- Use the system delegate directly when there is only one concrete playback implementation and no substitutable app capability.
Source map
| Source file |
Relevant symbols |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/MenuViewController.swift |
Playback-screen ownership and catalog metadata |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/VideoPlaybackViewController.swift |
Player lifecycle, KVO, metadata, policy, and AVKit delegate |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/CustomInteractiveVideoOverlayViewController.swift |
Overlay focus behavior |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/Base.lproj/Main.storyboard |
Menu and playback navigation |
TVAVPlayerViewControllerDemo/TVAVPlayerViewControllerDemo/Base.lproj/CustomInteractiveVideoOverlay.xib |
Overlay view hierarchy |