Sample CodetvOS 16.0+Reviewed 2026-07-21View on Apple Developer

Supporting remote interactions in tvOS

At a glance

Item Summary
Purpose Compare system-player, custom-player, remote-command, and guide handling for directional remotes on tvOS.
App architecture A menu launches independent UIKit scenarios; each controller owns a scenario-specific player and translates remote input into domain enums/commands.
Main patterns MVC, command mapping, weak delegate feedback, observation, singleton schedule.
Project style Feature folders (System Player, Custom Player, Remote Events, Guide) plus shared models, enums, and extensions.

Project structure

DirectionalRemotes/
├── MenuViewController.swift
├── System Player/
├── Custom Player/
│   ├── CustomPlayer.swift
│   ├── CustomPlayerViewController.swift
│   └── CustomPlayerView.swift
├── Remote Events/
├── Guide/
└── Shared/
    ├── Data/
    ├── RemoteCommand.swift
    ├── RemoteEvent.swift
    └── Extensions/

The archive is a suite of alternative examples, not one unified playback stack. Shared TVSchedule, RemoteCommand, and RemoteEvent types provide common vocabulary.

Overall architecture

Reference code

DirectionalRemotes/Custom Player/CustomPlayerViewController.swift:118 — the controller normalizes input before handing it to playback logic (abridged).

@objc private func playPausePressed() {
    customPlayer.remoteEventReceived(.playPause)
}

@objc private func channelUpPressed() {
    customPlayer.remoteEventReceived(.channelUp)
}

@objc private func dPadLeftPressed() {
    customPlayer.remoteEventReceived(.dPadLeft)
}

This controller-to-player command boundary is clearest in the custom scenario. The system scenario delegates most behavior to AVPlayerViewController; the remote-events scenario exposes command delivery instead.

Ownership and state

Ownership evidence

DirectionalRemotes/Custom Player/CustomPlayerViewController.swift:10 — the scenario controller owns and tears down its player (abridged).

class CustomPlayerViewController: UIViewController {
    private lazy var customPlayer = CustomPlayer()

    deinit {
        customPlayer.tearDown()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        customPlayer.delegate = self
    }
}
Owner Object or state Relationship Mutation authority
Scenario view controller Scenario player Lazy creates, retains, tears down Controller routes UI/lifecycle events; player mutates playback.
CustomPlayer Queue player, player state, KVO, command targets Creates and retains All custom transport/trick-play rules live here.
RemoteEventsPlayer Audio player and command targets Creates and retains Registers MediaPlayer commands and reports received events.
SystemPlayerViewController Queue player and AVKit controller Creates and embeds Handles only commands AVKit does not cover.
TVSchedule Channels/player items Process-wide singleton Private initializer; scenarios read shared catalog state.

Class and protocol design

Reference code

DirectionalRemotes/Custom Player/CustomPlayer.swift:10 — a narrow class-only delegate reports player results back to UI (abridged).

protocol CustomPlayerReporting: AnyObject {
    func reportFeedback(_ feedback: String,
                        withFeedbackType feedbackType: CustomPlayerFeedbackType)
}

class CustomPlayer {
    weak var delegate: CustomPlayerReporting?
}

The player does not know its concrete view controller. Similar RemoteEventsPlayerReporting and GuideReporting protocols keep other feature callbacks local.

Access control

Symbol Access Verified effect Likely rationale
Scenario player properties in controllers private lazy No other target code can replace scenario ownership. Couples lifetime and teardown to the presenting controller.
CustomPlayer.playerState, observers, helpers private Only the player implements its state machine and KVO lifecycle. Protects command/transport invariants.
delegate references internal weak Controllers can assign; players cannot retain UI. Necessary collaboration with cycle prevention.
TVSchedule.init private Only shared constructs schedule data. All scenarios see one catalog/order.
Gesture handlers @objc private Visible to Objective-C runtime selectors but not callable as app API. UIKit target-action integration with file-local intent.

DirectionalRemotes/Custom Player/CustomPlayer.swift:25:

class CustomPlayer {
    private var playerState: CustomPlayerState = .stopped
    let player: AVQueuePlayer
    weak var delegate: CustomPlayerReporting?
    private var rateObserver: NSKeyValueObservation?
    private var statusObserver: NSKeyValueObservation?
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Gesture/press recognition Scenario view controllers UIKit target-action belongs at the input boundary.
Custom transport and trick play CustomPlayer Framework-independent RemoteEvent values drive one state owner.
Media remote registration Player types Registration lifetime matches playback lifetime.
On-screen feedback Player reporting delegates and feature views Player reports meaning; UI chooses presentation.
Channel/program data Shared/Data/ Reused consistently across all scenarios.

Design patterns

Pattern Source evidence Purpose or tradeoff
MVC DirectionalRemotes/Custom Player/CustomPlayerViewController.swift:10 Controllers own input wiring and view composition.
Command mapping DirectionalRemotes/Custom Player/CustomPlayer.swift:100 Converts a finite remote-event vocabulary into transport behavior.
Delegate DirectionalRemotes/Custom Player/CustomPlayer.swift:13 Reports results to UI without a concrete controller dependency.
Observer DirectionalRemotes/Custom Player/CustomPlayer.swift:42 KVO starts command registration only when media is ready.
Shared catalog DirectionalRemotes/Shared/Data/TVSchedule.swift:14 Gives alternative scenarios identical content; introduces global state.

Naming conventions

  • Feature prefixes (CustomPlayer, RemoteEvents, Guide) keep scenario types distinct.
  • Reporting protocols describe callback direction: CustomPlayerReporting, GuideReporting.
  • Event/command enums separate physical input (RemoteEvent) from MediaPlayer command objects (RemoteCommand).
  • Setup/teardown methods are paired; target-action handlers end in Pressed or Action.

Architecture takeaways

  • Normalize physical input into small enums before applying playback rules.
  • Let scenario controllers own lifecycle while player objects own transport and command registration.
  • Use weak reporting protocols for playback-to-UI feedback.
  • Treat the four feature folders as alternatives; combining them into one architecture would misrepresent the sample.

Source map

Source file Relevant symbols
DirectionalRemotes/MenuViewController.swift Scenario routing
DirectionalRemotes/Custom Player/CustomPlayer.swift Command/state owner, reporting protocol
DirectionalRemotes/Custom Player/CustomPlayerViewController.swift Input boundary/lifetime
DirectionalRemotes/Remote Events/RemoteEventsPlayer.swift Media remote registration
DirectionalRemotes/System Player/SystemPlayerViewController.swift AVKit-based alternative
DirectionalRemotes/Shared/Data/TVSchedule.swift Shared catalog