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
flowchart LR
Menu["MenuViewController"] --> System["SystemPlayerViewController"]
Menu --> CustomVC["CustomPlayerViewController"]
Menu --> EventsVC["RemoteEventsViewController"]
Menu --> Guide["GuideViewController"]
CustomVC --> Custom["CustomPlayer"]
EventsVC --> Events["RemoteEventsPlayer"]
System --> AVKit["AVPlayerViewController"]
Custom --> Queue["AVQueuePlayer"]
Events --> Commands["MPRemoteCommandCenter"]
System --> Schedule["TVSchedule.shared"]
Custom --> Schedule
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
classDiagram
CustomPlayerViewController *-- CustomPlayer
CustomPlayer *-- AVQueuePlayer
CustomPlayer --> TVSchedule : shared data
RemoteEventsViewController *-- RemoteEventsPlayer
RemoteEventsPlayer *-- AVQueuePlayer
SystemPlayerViewController *-- AVQueuePlayer
SystemPlayerViewController *-- AVPlayerViewController
GuideViewController --> TVSchedule : shared data
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
classDiagram
class CustomPlayerReporting {
<<protocol>>
reportFeedback()
}
CustomPlayerViewController ..|> CustomPlayerReporting
CustomPlayer --> CustomPlayerReporting : weak delegate
class RemoteEventsPlayerReporting {
<<protocol>>
playerReceived()
}
RemoteEventsViewController ..|> RemoteEventsPlayerReporting
RemoteEventsPlayer --> RemoteEventsPlayerReporting : weak delegate
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
PressedorAction.
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 |