Sample CodeiOS, iPadOS, Mac Catalyst, tvOSReviewed 2026-07-21View on Apple Developer

Adopting Picture in Picture Playback in tvOS

At a glance

Item Summary
Purpose Compares system AVPlayerViewController Picture in Picture with a custom player-layer implementation.
App architecture A menu controller selects one of two player strategies and centralizes dismissal/restoration callbacks.
Main patterns Runtime strategy selection, weak delegates, custom composition, explicit PiP lifecycle retention.
Project style Single cross-platform target with focused menu, model, controls, and custom-player files.

Project structure

PictureInPictureSample/
├── ViewController.swift
├── PlaybackItem.swift
├── ButtonGridView.swift
├── CustomPlayerViewController.swift
├── CustomPlayerControlsView.swift
└── AppDelegate.swift

Structure observations

  • PlaybackItem combines video choice and player kind, letting one menu exercise both implementations.
  • The custom player separates playback/PiP ownership from transient control rendering.

Overall architecture

Reference code

PictureInPictureSample/ViewController.swift:32 — the root selects a system or custom player behind the same presentation flow.

func play(item: PlaybackItem) {
    let player = AVQueuePlayer(playerItem: item.video.playerItem)
    let playerViewController: UIViewController
    switch item.playerKind {
    case .avPlayerViewController:
        let controller = AVPlayerViewController()
        controller.player = player
        controller.delegate = self
        playerViewController = controller
    case .customPlayerViewController:
        let controller = CustomPlayerViewController()
        controller.player = player
        controller.delegate = self
        playerViewController = controller
    }
}

Interpretation

The root owns navigation policy, while each player owns its presentation technology. Both report PiP dismissal/restoration through delegate APIs, allowing common restoration logic.

Ownership and state

Ownership evidence

PictureInPictureSample/CustomPlayerViewController.swift:23 — the custom controller owns PiP infrastructure but delegates navigation policy.

class CustomPlayerViewController: UIViewController {
    weak var delegate: CustomPlayerViewControllerDelegate?
    var player: AVPlayer? {
        didSet { playerLayer = AVPlayerLayer(player: player) }
    }
    private var playerLayer: AVPlayerLayer?
    private var pictureInPictureController: AVPictureInPictureController?
    private var controlsView: CustomPlayerControlsView?
}
Owner Object or state Relationship Mutation authority
ViewController Selection, player instance, presented controller Creates and presents Root controller
Custom player Layer, PiP controller, controls Creates and retains Custom player callbacks
File-private active set Custom players in PiP transition/active state Temporarily retains PiP delegate methods
Controls view Buttons/progress timer Owns UI state; weakly references player/PiP Controls and delegate

Class and protocol design

Type Responsibility Depends on
ViewController Choose player style and restore full-screen UI Both player delegates
PlaybackItem Map a grid index to player kind and video AVPlayerItem creation
CustomPlayerViewController Compose layer, PiP controller, and controls AVKit/UIKit
CustomPlayerControlsViewDelegate Convert control taps into player actions Custom player
CustomPlayerViewControllerDelegate Ask host about dismissal/restoration Root navigation policy
ButtonGridViewDelegate Supply titles and selection handling Root controller

Access control

Symbol Access Verified effect Likely rationale
activeCustomPlayerViewControllers file-level private Only PiP implementation code can retain/release active custom players. Prevent premature deallocation without global app exposure.
Custom player’s layer/PiP/controls private Host cannot partially mutate the composed player. Preserve lifecycle coordination.
Player delegates weak Player and controls do not retain the presenting owner. Avoid presentation cycles.
Button/label helper types private extensions/nested classes Styling implementation stays local. Reduce module surface.
App types implicit internal No external module API. Sample is an app target.

Reference code

PictureInPictureSample/CustomPlayerViewController.swift:154 — lifecycle callbacks are the sole mutation path for the private retention set.

func pictureInPictureControllerWillStartPictureInPicture(_ controller: AVPictureInPictureController) {
    activeCustomPlayerViewControllers.insert(self)
}

func pictureInPictureControllerDidStopPictureInPicture(_ controller: AVPictureInPictureController) {
    activeCustomPlayerViewControllers.remove(self)
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Player selection/presentation ViewController Navigation policy.
System-player PiP AVPlayerViewController with root delegate Framework supplies UI/lifecycle.
Custom PiP composition CustomPlayerViewController Owns layer/controller coupling.
Control rendering and focus CustomPlayerControlsView Reusable transient UI.
Restoration Root restore method Same behavior for both strategies.
Selection model PlaybackItem Pure mapping from grid to playback choice.

Design patterns

Pattern Source evidence Purpose or tradeoff
Strategy selection PictureInPictureSample/ViewController.swift:38 Compares system and custom player implementations through one flow.
Delegate PictureInPictureSample/CustomPlayerViewController.swift:15 Keeps PiP component independent of navigation policy.
Composition PictureInPictureSample/CustomPlayerViewController.swift:54 Builds a custom player from AVKit primitives.
Lifecycle retention PictureInPictureSample/CustomPlayerViewController.swift:13 Keeps delegate/controller alive while full-screen UI is dismissed.
Value model PictureInPictureSample/PlaybackItem.swift:10 Encodes the two selection dimensions without conditional UI state.

Naming conventions

  • Custom prefixes distinguish app-owned player/control implementations from AVKit types.
  • Delegate callbacks use event/intent phrasing: DidRequest, ShouldAutomaticallyDismiss, restoreUserInterface.
  • PlayerKind, PlaybackItem, and Video name domain choices rather than UI widgets.
  • Files match their principal type and responsibility.

Architecture takeaways

  • Centralize restoration policy when multiple player implementations support the same lifecycle.
  • A custom PiP player must own both the player layer and PiP controller as a coordinated unit.
  • Weak delegates solve cycles, but an active PiP flow may still require deliberate temporary retention.
  • Use a value model to select implementation strategies without spreading conditionals across views.

Source map

Source file Relevant symbols
PictureInPictureSample/ViewController.swift Strategy selection, presentation, restoration
PictureInPictureSample/PlaybackItem.swift Player/video model
PictureInPictureSample/CustomPlayerViewController.swift Custom layer/PiP composition and lifecycle
PictureInPictureSample/CustomPlayerControlsView.swift Control delegate and focus UI
PictureInPictureSample/ButtonGridView.swift Menu capability protocol