在 tvOS 上活下來:一個非典型播放器的工程實錄

在苹果庞大的硬件生态中,除了 iPhone 和 MacBook 这些自带光环的明星产品外,还有一些设备在其特定领域占据着统治地位,却往往被开发者的视野所忽略。Apple TV 显然就是其中最典型的代表——它存在感不强,让很多开发者对其“投入产出比”心存疑虑。

由于 SwiftUI 的跨平台特性,我曾天真地以为开发 tvOS 应用只是 iOS 开发的简单延伸。但在过去两年与 Ronnie Wong 的交流中,我才意识到 tvOS 对于大多数开发者来说,无论从设计理念、硬件限制,还是开发调试细节,都是一个充满挑战的全新领域。

因此,我特邀 Ronnie Wong 分享她在开发 Syncnext 过程中的工程实录。这篇文章不仅展现了苹果生态中这个“小众”平台的独特开发场景,更是她在这个领域“活下来”的经验总结。希望能借此通过真实的视角,让更多开发者了解 Apple TV 开发的苦与乐。

如果你只是寫一個 Demo,tvOS 看起來其實不難;但只要嘗試做一個需要長期維護、面對真實用戶環境的 App,很快就會發現:tvOS 從來就不是 iPad 的放大版

它幾乎沒有可靠的本地持久儲存,隨時可能被系統清空資料或殺掉行程;整個交互模型以 Focus 為中心,沒有瀏覽器、沒有 WebView,卻又在播放器層面深度依賴整個 Web 世界。從設計上來看,tvOS 本質上是一個展示終端,而不是工作平台。

Syncnext 正好是一個非典型的例子:它不是服務單一後端的定制播放器,而是在 tvOS 上嘗試面對真實而混亂的網路環境。這篇文章不是教學,而是一次工程實錄,整理那些在這個平台上「活下來」所付出的設計取捨與現實妥協。

如果你正在考慮做一個不只是 Demo 的 tvOS App,這些經驗或許能幫你少走一些彎路。

一、核心交互哲學:非接觸式操作

tvOS 的 UI/UX 概念,與「觸摸式」系統或「鼠標模式」系統截然不同。你需要使用 Game UX 的概念來思考。在主機平台上,手柄的交互模式才是開發 tvOS App 的首要參考對象。

1. Focus Engine vs. SwiftUI @FocusState

在 tvOS 開發中,你會遇到兩套焦點系統:

  • Focus Engine: 以 UIKit 為中心,專為 tvOS 設計的原生方案。
  • SwiftUI @FocusState: 以 Apple 全平台為中心,跨平台的狀態管理系統。

請注意,它們是完全不同概念的產品。 雖然交互形式相似,但邏輯不同。

  • 常見陷阱:你可以在 UI 上同時使用 @FocusState 和 Focus Engine 將兩個焦點高亮,但系統認可的「真實焦點」只是你最後操作的那個指令。

2. 深入支持遙控器

tvOS 的遙控器主要分為兩代:

  1. Siri Remote (1st Gen): 觸控板為主。
  2. Siri Remote (2nd Gen): 實體按鍵 + 觸控環。

如果你不在意那些喜歡舊款遙控器的老用戶,完全可以使用 SwiftUI 的標準修飾符。但如果你追求極致體驗,你需要使用 Game Controller Framework 深入到 RAW 層去處理輸入訊號。

二、系統限制與生存指南

1. 嚴苛的存儲環境

  • 沒有持久儲存:你沒有寫入 Document 目錄的權限,只能寫入 Cache 文件夾。
  • 數據隨時遺失:當 tvOS 監測到存儲空間緊張時,系統會隨機刪除 App 的 Cache 數據。如果你的 App 以本地 SQLite 數據庫為核心,這將是災難性的。
    • 常見現象:「因為你的 Apple TV 空間不足,所以刪除了這個 App 的數據庫。」
    • 建議檢查:鳥瞰壁紙/iCloud 圖片是否開啟?Infuse/SenPlayer 的緩存是否設置為內存模式?

2. 缺失的 Web 能力

  • 沒有瀏覽器:tvOS 沒有 WebView,只有一個 JavaScriptCore 框架。依賴 DOM 運作的概念在這裡無法實現。

3. 網絡與設備差異

  • IPv6 優先:在同一局域網下,可能出現手機可訪問 API,但 tvOS 因 IPv6 策略而無法訪問的情況。
  • 算力斷層:Apple TV 4K (2017, 2021, 2022) 之間的性能差異巨大。如果你做了酷炫的視覺效果,請務必為老設備提供「關閉特效」的選項。

4. 本地化與故障排除

  • 無簡體中文 Siri:使用 Siri 語音輸入時,系統只會識別並輸出「繁體中文」。如果你的 API 需要簡體中文,必須在 App 內置「繁簡轉換」系統。
  • 終極大法:如果 App 在某些用戶設備上無法啟動(黑屏或閃退),這通常不是你的代碼問題。請建議用戶 「拔電重啟」 Apple TV。

三、SwiftUI 在 tvOS 上的實戰填坑

1. 輸入框的「影子戰術」

SwiftUI 的 TextField 在 tvOS 上既醜又難用。解決方案是**「影子輸入法」**:

  • UI 層:用漂亮的 Button 製作 UI。
  • 功能層:在視圖背景放置一個透明的 CocoaTextField (UIKit)。
  • 交互:點擊按鈕時,調用 textField.becomeFirstResponder() 喚起鍵盤。

2. 導航與狀態管理

  • Searchable 節流:tvOS 會根據輸入頻率觸發搜索,務必實施節流 (Throttle) 並緩存結果。
  • 自定義 NavigationLink:SwiftUI 原生導航缺乏靈活性,建議封裝 .navigate(using:destination:) 修飾符,實現「狀態驅動」的導航跳轉。

3. TVUIKit 的必要性

SwiftUI 的 .card 樣式往往給人一種「山寨」感。要開發具有官方質感的 App,你需要封裝 TVUIKit 組件:

  • 排版:用 UILabel 實現精準行高控制;用 UITextView 實現遙控器滾動閱讀。
  • 視覺:用 TVPosterView 獲取原生的視差與聚焦效果。
  • 輸入:用 TVDigitEntryViewController 實現原生的數字密碼輸入。

4. 焦點系統的混用災難

在 tvOS 上混用 SwiftUI 與 UITableView 時,SwiftUI 的焦點預測機制常與列表實際高度衝突,導致焦點錯位。解決方案通常是直接接管 UITableView 的 Focus 系統。

5. 開發者體驗優化

四、播放器核心技術細節

1. 絲滑的進度條交互

基於 UIPanGestureRecognizer 實現。電視遙控器的觸控板不是鼠標,不能直接映射位移。

  • 虛擬阻尼:設置 location / 5 的比例係數。這個「手感值」將位移進行非線性縮放,確保在大長度視頻中也能精確定位到秒。
  • 實時反饋:滑動時狀態需直接驅動 UI Overlay,實現「所見即所得」。

2. 雙重 UI 疊加與 Back 鍵邏輯

Menu (Back) 鍵的邏輯是多維的,需要一個狀態機:

  1. 狀態 A (UI 顯示中):如果焦點在按鈕上,按 Menu 鍵 -> 焦點回到進度條。
  2. 狀態 B (焦點在進度條):按 Menu 鍵 -> 隱藏 UI。
  3. 狀態 C (UI 已隱藏):按 Menu 鍵 -> 退出播放。

3. AVPlayer 調校

AVPlayer 默認是一個「暴力」的網絡播放器,需要針對性調優:

  • 降低延遲
Swift
player?.automaticallyWaitsToMinimizeStalling = true
player?.currentItem?.preferredForwardBufferDuration = 0 // 直播/串流建議
  • 本地文件特化
Swift
// 若是本地文件
automaticallyWaitsToMinimizeStalling = false
preferredForwardBufferDuration = 1
  • HLS 支持:AVPlayer 強制要求 HTTP(s) 協議。如果你需要動態重建 m3u8,必須使用 Local Server (推薦 Swifter 框架)。

  • 動態背景:若將 AVPlayer 用於動態背景,務必設置 AVAudioSession.ambient 以免打斷系統音頻。

4. 自定義 Dismiss 行為

在 tvOS 新版本中,AVPlayer 對 dismiss 的響應發生了變化。建議重寫 dismiss 邏輯,依序檢查: presentingViewController (dismiss) -> navigationController (pop) -> removeFromParent

五、數據同步策略

不推薦: CoreData with CloudKit

在 2026 年的視角下,我不建議使用 CoreData with CloudKit 方案。它始終無法解決空間膨脹的問題,且在無持久存儲的 tvOS 上風險極高。

肘子注

这里所说的空间膨胀,指的是数据经由 NSPersistentCloudKitContainer 同步后,云端占用往往会远超本地 SQLite 数据库的大小。这是由类型转换、元数据开销等原因造成的。一旦你的应用需要保存大量二进制数据,这个问题就会被成倍放大,甚至可能导致用户的 iCloud 空间爆满。

当时我们在 Discord 上对此有过专门的讨论

推薦:sqlite-data

我推薦使用 sqlite-data 作為 iCloud 支持方案。它在 TracklyReborn 項目中經過測試,表現穩定且可控。

六、後記:關於 eisonAI

在結束這篇硬核的 tvOS 工程實錄前,想介紹一下我的最新作品 —— eisonAI

iPhone-Medata-Preview.jpg

這是一個關於「記憶」與「心流」的 App。我們常遇到看過東西卻找不到、想法一寫就亂的情況。eisonAI 引入了 Cognitive Index™(認知索引) 的概念:不只記住「內容」,更記住「用途」。它就像你的腦內圖書館管理員,幫你把靈感、背景資料、引用文獻自動歸位,保護你的思考心流不被打斷。

About Author

Ronnie Wong 是獨立開發者,長期在 Apple 平台(iOS / macOS / tvOS)進行系統級與產品級開發,關注播放器架構、系統邊界問題、本地優先(local-first)工具鏈與 AI 自動化管線。開發過 Syncnext(tvOS 通用播放器)、aDict、TracklyReborn、eisonAI,以及多個本地 AI 自動化系統(如 HLN Machine)。近期工作重心包括 LLM 工具鏈、自動化內容生產管線與語義穩定性(Semantic Entropy)相關工程實踐。本文整理自 Syncnext 在 tvOS 上的多年實戰經驗。

GitHub: https://github.com/qoli

订阅 Fatbobman 周报

每周精选 Swift 与 SwiftUI 开发技巧,加入众多开发者的行列。

立即订阅