关于 axiom-tvos
供开发者构建或移植 tvOS 应用的参考技能,涵盖焦点、Siri Remote 输入、存储和网页内容等平台差异,并指出常见运行时问题。
主要功能
- 01UIKit 与 SwiftUI 焦点系统
- 02Siri Remote 输入处理
- 03tvOS 存储限制
- 04网页内容替代方案
- 05文本输入和 AVPlayer 注意事项
适用场景
- 01将 iOS 应用移植到 tvOS
- 02排查焦点导航问题
- 03规划应用数据持久化
- 04替换基于 WebView 的内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-tvos安装后在 Claude Code 中输入/axiom-tvos确认已加载。
详细介绍
tvOS 开发
概述
tvOS 与 iOS 共用 UIKit 和 SwiftUI,但在一些关键方面有所不同,常让 iOS 开发者措手不及。最危险的三个误区:(1)本地文件会持久保存;(2)存在 WebView;(3)焦点的工作方式与 @FocusState 相同。
核心原则 tvOS 不是“电视上的 iOS”。它有双重焦点系统,没有持久本地存储,没有 WebView,且遥控器有两代互不兼容的版本。请将其视为独立平台。
tvOS 26 采用 Liquid Glass 设计语言,并推出全新的 App 图标系统。实现模式请参阅 axiom-liquid-glass。
tvOS 移植检查
发布 tvOS 移植版本前,请检查以下五个方面——它们涵盖了 90% 的 tvOS 特有错误:
| 方面 | 检查项 | 章节 |
|---|---|---|
| 存储 | 没有持久本地文件——需要 iCloud | §3 |
| 焦点 | 双重系统正常工作,间隙处使用焦点引导 | §1 |
| WebView | 已替换为 JavaScriptCore 或原生渲染 | §4 |
| 文本输入 | 已处理影子输入或全屏键盘 | §6 |
| AVPlayer | 音频会话、缓冲区、Menu 按钮状态机 | §7,§8 |
“能在 tvOS 上编译”毫无意义。这五个方面都能顺利编译,却会在运行时出错。
何时使用此技能
- 构建新的 tvOS 应用或添加 tvOS 目标
- 将 iOS 应用移植到 tvOS
- 调试 tvOS 上的焦点、遥控器输入或存储问题
- 使用 tvOS 上的 AVPlayer、TVUIKit 或文本输入
示例提示词
以下是开发者常问的真实问题,本技能将解答这些问题:
1. “我正在将 iOS 应用移植到 tvOS,但焦点导航无法正常工作”
-> 本技能会讲解双重焦点系统(UIKit Focus Engine 与 @FocusState)和常见陷阱
2. “我的 tvOS 应用在每次启动之间都会丢失所有数据”
-> 本技能会说明 tvOS 没有持久本地存储,并展示以 iCloud 为先的模式
3. “如何在 tvOS 上用 SwiftUI 处理 Siri Remote 输入?”
-> 本技能涵盖两代遥控器和三种输入层(SwiftUI、UIKit 手势、GameController)
4. “WebView 在 tvOS 上无法工作,我该如何显示网页内容?”
-> 本技能展示用于解析的 JavaScriptCore 和原生渲染替代方案
危险信号
如果出现以下任一情况,请立即停止:
- “我直接沿用 iOS 的存储代码就行”——tvOS 没有 Document 目录
- “用 WebView 就能实现”——tvOS 完全没有 WebView(Apple HIG:“tvOS 不支持”)
- “@FocusState 可以处理焦点”——tvOS 有双重焦点系统;仅使用 @FocusState 并不完整
- “我会保存到 Application Support”——该目录仅用于缓存;应用未运行时,系统会删除文件
- “标准 UITextField 就能用”——tvOS 文本输入会触发全屏键盘;请考虑影子输入模式
- “我直接沿用 iOS 的 AVPlayer 代码就行”——tvOS 启动时需要 .ambient 音频会话、自定义 Menu 按钮处理和缓冲区调优。默认的 iOS AVPlayer 设置会导致音频会话冲突和返回导航异常。
1. Focus Engine 与 @FocusState
tvOS 有两个必须共存的焦点系统。这是 iOS 开发者最常感到困惑的地方。
双重系统
| 系统 | 控制内容 | API |
|---|---|---|
| UIKit Focus Engine | 硬件遥控器导航、方向扫描 | UIFocusEnvironment、UIFocusSystem、UIFocusGuide |
| SwiftUI Focus | 程序化焦点绑定、焦点区域 | @FocusState、.focused()、.focusable()、.focusSection() |
各自适用场景
User swipes on remote → UIKit Focus Engine handles it (always)
Code sets @FocusState → SwiftUI handles it (sometimes overridden by Focus Engine)
陷阱:@FocusState 可以以编程方式设置焦点,但 UIKit Focus Engine 才拥有最终决定权。如果 Focus Engine 认为某个视图不可聚焦,就会静默忽略 @FocusState 赋值。
UIKit Focus Engine API
UIFocusEnvironment 协议(由 UIView、UIViewController、UIWindow 实现)提供:
class MyViewController: UIViewController {
// Priority-ordered list of where focus should go
override var preferredFocusEnvironments: [UIFocusEnvironment] {
[preferredButton, fallbackButton]
}
// Validate proposed focus changes
override func shouldUpdateFocus(
in context: UIFocusUpdateContext
) -> Bool {
// Return false to block focus movement
return context.nextFocusedView != disabledButton
}
// Respond to completed focus changes
override func didUpdateFocus(
in context: UIFocusUpdateContext,
with coordinator: UIFocusAnimationCoordinator
) {
coordinator.addCoordinatedAnimations {
context.nextFocusedView?.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
context.previouslyFocusedView?.transform = .identity
}
}
// Request focus update (async)
func moveFocusToPreferred() {
setNeedsFocusUpdate() // Schedule update
updateFocusIfNeeded() // Execute immediately
}
}
UIFocusGuide——弥合导航间隙
当可聚焦视图不在直接的网格布局中时,Focus Engine 无法通过方向扫描找到它们。UIFocusGuide 会创建不可见的可聚焦区域,将焦点重定向到真实视图:
let focusGuide = UIFocusGuide()
view.addLayoutGuide(focusGuide)
// Position the guide between two non-adjacent views
NSLayoutConstraint.activate([
focusGuide.leadingAnchor.constraint(equalTo: leftButton.trailingAnchor),
focusGuide.trailingAnchor.constraint(equalTo: rightButton.leadingAnchor),
focusGuide.topAnchor.constraint(equalTo: leftButton.topAnchor),
focusGuide.heightAnchor.constraint(equalTo: leftButton.heightAnchor)
])
// When focus enters the guide, redirect to the target view
focusGuide.preferredFocusEnvironments = [rightButton]
SwiftUI 焦点 API
struct ContentView: View {
@FocusState private var focusedItem: MenuItem?
var body: some View {
VStack {
ForEach(MenuItem.allCases) { item in
Button(item.title) { select(item) }
.focused($focusedItem, equals: item)
}
}
.focusSection() // Group focusable items for navigation
.defaultFocus($focusedItem, .home) // Set initial focus
}
}
tvOS 的关键 SwiftUI 焦点修饰符:
.focused(_:equals:)— 将焦点绑定到某个值.focusable()— 使自定义视图可获得焦点.focusSection()— 将相关项目分组,以便进行方向导航.defaultFocus(_:_:)— 设置焦点在某个作用域中的起始位置
默认可聚焦元素
UIButton、UITextField、UITableViewCell 和 UICollectionViewCell 默认都可聚焦。自定义视图需要实现 canBecomeFocused(UIKit)或使用 .focusable()(SwiftUI)。应用启动时,左上角的项目会获得初始焦点。
常见焦点问题
| 问题 | 表现 | 修复方法 |
|---|---|---|
| 容器不可聚焦 | 滑动时会跳过视图 | 添加 .focusable() 或重写 canBecomeFocused |
| 缺少焦点引导 | 无法导航到孤立视图 | 添加 UIFocusGuide 以连接间隙 |
| @FocusState 被忽略 | 以编程方式设置焦点不起作用 | 检查 preferredFocusEnvironments 链 |
| 未请求更新焦点 | 布局更改后焦点仍然过时 | 调用 setNeedsFocusUpdate() + updateFocusIfNeeded() |
| 项目未按网格布局 | 焦点跳转不可预测 | 将可聚焦项目排列成网格,或使用焦点引导 |
| UIHostingConfiguration 焦点问题 | UIKit/SwiftUI 混合使用时焦点错乱 | 已知问题 — 仔细测试 UIHostingConfiguration 单元格 |
2. Siri Remote 输入
有两代硬件,二者有所不同 — 代码必须兼顾两者。
两代产品的差异
| 功能 | 第 1 代(2015-2021) | 第 2 代(2021+) |
|---|---|---|
| 顶部表面 | 触控板(整个区域均可滑动) | 点击板 + 外圈触控环 |
| 滑动手势 | 整个区域 | 仅环形边缘 |
| 点击导航 | 按下中心 | D-pad 样式 |
| 加速度计 | 有 | 有 |
标准 SwiftUI 修饰符(首选)
对于大多数 UI,SwiftUI 会通过焦点系统自动处理遥控器输入:
Button("Play") { startPlayback() }
.focused($isFocused) // Automatically responds to remote navigation
List(items) { item in
Text(item.title)
}
// List navigation works automatically with remote
// Note: First item receives focus by default on tvOS — use .defaultFocus() to override
手势识别器(UIKit)
通过 UIKit 识别器检测特定的按键和手势:
// Detect Play/Pause button
let playPause = UITapGestureRecognizer(target: self, action: #selector(handlePlayPause))
playPause.allowedPressTypes = [NSNumber(value: UIPress.PressType.playPause.rawValue)]
view.addGestureRecognizer(playPause)
// Detect swipe on touchpad
let swipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe))
swipe.direction = .right
view.addGestureRecognizer(swipe)
可用的 UIPress.PressType 值:.menu、.playPause、.select、.upArrow、.downArrow、.leftArrow、.rightArrow、.pageUp、.pageDown
低级按键处理
如需精细控制,可重写 UIResponder 的按键处理方法:
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
for press in presses {
if press.type == .select {
handleSelectDown()
}
}
}
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
for press in presses {
if press.type == .select {
handleSelectUp()
}
}
}
// Always implement all four: pressesBegan, pressesEnded, pressesChanged, pressesCancelled
游戏控制器框架(原始输入)
对于自定义交互(拖动进度、游戏),可通过 GCMicroGamepad 访问 Siri Remote:
import GameController
NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect, object: nil, queue: .main
) { notification in
guard let controller = notification.object as? GCController,
let micro = controller.microGamepad else { return }
// Touchpad as analog D-pad (-1.0 to 1.0)
micro.dpad.valueChangedHandler = { _, xValue, yValue in
handleRemoteInput(x: xValue, y: yValue)
}
// reportsAbsoluteDpadValues: true = absolute position, false = relative movement
micro.reportsAbsoluteDpadValues = false
// allowsRotation: true = values adjust when remote is rotated
micro.allowsRotation = false
// Face buttons
micro.buttonA.pressedChangedHandler = { _, _, pressed in }
micro.buttonX.pressedChangedHandler = { _, _, pressed in }
micro.buttonMenu.pressedChangedHandler = { _, _, pressed in }
}
进度条拖动
使用带有虚拟阻尼的 UIPanGestureRecognizer,实现流畅的进度定位:
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
@objc func handlePan(_ gesture: UIPanGestureRecognizer) {
let velocity = gesture.velocity(in: view)
let dampingFactor: CGFloat = 0.002 // Tune for feel
switch gesture.state {
case .changed:
let seekDelta = velocity.x * dampingFactor
player.seek(to: currentTime + seekDelta)
default:
break
}
}
3. 存储限制
这是在 tvOS 上最危险的 iOS 假设。 tvOS 没有 Documents 目录。所有本地存储都是缓存,系统可以随时删除。跳过 iCloud 集成,可能会导致花费 2-3 周时间排查间歇性“数据消失”问题;这类问题只会在真实设备上、应用启动之间发生。
Apple 的《tvOS 应用编程指南》指出:“为新 Apple TV 开发的每个应用都必须能够在 iCloud 中存储数据,并以提供优质客户体验的方式检索数据。”
tvOS 提供的内容
| 目录 | 是否存在? | 是否持久? |
|---|---|---|
| Documents | 否 | 不适用 |
| Application Support | 是 | 否 — 应用未运行时,系统可以删除 |
| Caches | 是 | 否 — 存储空间紧张时,系统会删除 |
| tmp | 是 | 否 |
大小限制
- 应用包:最大 4 GB
- NSUserDefaults / UserDefaults:最大 500 KB(Apple 文档记载;并非 iOS 的 4 MB)。可用,但可能被系统清除 — 无法保证在不同会话之间持久保存
- 按需资源:可用于由操作系统管理的只读资源
- 本地缓存:大小不保证;应用未运行时,系统可以清除
这意味着什么
- 每个本地文件都可能在应用启动之间消失
- 本地存储的 SQLite 数据库会被删除
- 应用必须能在没有任何本地数据的情况下运行
- 应用运行期间,下载的数据不会被删除 — 只会在会话之间删除
推荐模式
// ✅ CORRECT: iCloud as primary, local as cache only
func loadData() async throws -> [Item] {
// 1. Try iCloud first (persistent)
if let cloudData = try? await fetchFromICloud() {
// Cache locally for offline use
try? cacheLocally(cloudData)
return cloudData
}
// 2. Fall back to local cache (may not exist)
if let cached = try? loadFromLocalCache() {
return cached
}
// 3. Start fresh — this is normal on tvOS
return []
}
数据库建议
| 方案 | tvOS 适用性 | 说明 |
|---|---|---|
| SQLiteData + CloudKit SyncEngine | 推荐 | iCloud 持久化存储;本地仅作缓存 |
| SwiftData + CloudKit | 可用,但不稳定 | 不支持仅限本地的持久化存储;从一开始就必须为 CloudKit 配置 ModelContainer——之后添加同步需要迁移;系统删除数据库后,下次启动时会触发完整重新同步 |
| CoreData + CloudKit | 有风险 | CloudKit 元数据会导致空间膨胀 |
| 仅限本地的 GRDB/SQLite | 不可靠 | 系统会删除数据库文件 |
| NSUbiquitousKeyValueStore | 适合少量数据 | 限制为 1 MB,且仅支持键值数据 |
| 按需资源 | 适合只读资源 | 下载和清理生命周期由操作系统管理 |
参见 axiom-sqlitedata 了解 CloudKit SyncEngine 模式,参见 axiom-storage 查看完整的存储决策树。
4. 不使用 WebView
tvOS 不支持 WKWebView、SFSafariViewController 或 WebView。Apple HIG 明确指出:tvOS“不支持”Web 视图。
可以采用的方案
| 需求 | 方案 |
|---|---|
| 解析 HTML/JSON | 使用 JavaScriptCore(JSContext、JSValue——无 DOM) |
| 显示 Web 内容 | 根据解析后的数据进行原生渲染 |
| 通过 m3u8 进行 HLS 流式播放 | 使用本地 HTTP 服务模式(见下文) |
| OAuth 登录 | 使用设备码流程(RFC 8628)或配套设备 |
使用 JavaScriptCore 进行解析
JavaScriptCore 提供 JavaScript 执行引擎,不含 DOM 或 Web 渲染功能。tvOS 上可用。
import JavaScriptCore
let context = JSContext()!
// Evaluate scripts
context.evaluateScript("""
function parsePlaylist(m3u8Text) {
return m3u8Text.split('\\n')
.filter(line => !line.startsWith('#'))
.filter(line => line.trim().length > 0);
}
""")
// Pass data safely via setObject (avoids injection)
context.setObject(m3u8Content, forKeyedSubscript: "rawContent" as NSString)
let result = context.evaluateScript("parsePlaylist(rawContent)")
// Convert back to Swift types
let segments = result?.toArray() as? [String] ?? []
主要类:JSVirtualMachine(执行环境)、JSContext(脚本求值)、JSValue(类型桥接)
限制:无 DOM、无 Web 渲染、无 fetch/XMLHttpRequest。仅支持纯 JavaScript 执行。
用于 HLS 的本地 HTTP 服务
需要向 AVPlayer 提供经过修改的 m3u8 播放列表时:
// Use Swifter (httpswift/swifter) or GCDWebServer
// Serve rewritten m3u8 on localhost, point AVPlayer to it
let localURL = URL(string: "http://localhost:8080/playlist.m3u8")!
let playerItem = AVPlayerItem(url: localURL)
5. TVUIKit 组件
tvOS 专属 UIKit 组件。通过 UIViewRepresentable 接入 SwiftUI。
TVPosterView
媒体内容展示组件,内置焦点放大和视差效果:
import TVUIKit
let poster = TVPosterView(image: UIImage(named: "moviePoster"))
poster.title = "Movie Title"
poster.subtitle = "2024"
// Focus expansion and parallax happen automatically
// Access the underlying image view:
poster.imageView.adjustsImageWhenAncestorFocused = true
TVLockupView
TVPosterView 的基类——用于管理内容和焦点行为的灵活容器:
let lockup = TVLockupView()
lockup.contentView.addSubview(customView)
lockup.headerView = headerFooter // TVLockupHeaderFooterView
lockup.footerView = footerFooter
// showsOnlyWhenAncestorFocused: header/footer visibility on focus
其他 TVUIKit 组件
| 组件 | 用途 |
|---|---|
| TVCardView | 带可自定义背景的简单容器 |
| TVCaptionButtonView | 带图片、文本和方向性视差效果的按钮 |
| TVMonogramView | 使用 PersonNameComponents 显示用户姓名缩写/图像 |
| TVCollectionViewFullScreenLayout | 带视差和遮罩效果的沉浸式全屏集合视图 |
| TVMediaItemContentView | 配置包含徽标和播放进度的内容 |
TVDigitEntryViewController
系统提供的密码/PIN 输入界面(tvOS 12+):
let digitEntry = TVDigitEntryViewController()
digitEntry.numberOfDigits = 4
digitEntry.titleText = "Enter PIN"
digitEntry.promptText = "Enter your parental control code"
digitEntry.isSecureDigitEntry = true
present(digitEntry, animated: true)
digitEntry.entryCompletionHandler = { pin in
guard let pin else { return } // User cancelled
authenticate(with: pin)
}
// Reset entry
digitEntry.clearEntry(animated: true)
6. tvOS 上的文本输入
tvOS 的文本输入方式与 iOS 有本质区别。Apple 建议尽量减少 UI 中的文本输入。
三种方式
| 方式 | 最适用于 | 键盘样式 |
|---|---|---|
| UIAlertController | 快速、简单的输入 | 带文本字段的模态界面 |
| UITextField | 多字段表单 | 带“下一项”/“上一项”按钮的全屏键盘 |
| UISearchController | 搜索 | 行内单行键盘 |
UITextField(全屏键盘)
主要的文本输入方式。调用 becomeFirstResponder() 会显示全屏键盘:
let textField = UITextField()
textField.placeholder = "Enter name"
textField.becomeFirstResponder() // Presents keyboard immediately
// Done button returns user to previous page
// Built-in Next/Previous buttons navigate between text fields
影子输入模式(SwiftUI)
要在 SwiftUI 中使用自定义样式的输入触发器:
struct TVTextInput: View {
@State private var text = ""
@State private var isEditing = false
var body: some View {
Button {
isEditing = true
} label: {
HStack {
Text(text.isEmpty ? "Search..." : text)
.foregroundStyle(text.isEmpty ? .secondary : .primary)
Spacer()
Image(systemName: "keyboard")
}
.padding()
.background(.quaternary)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
.sheet(isPresented: $isEditing) {
TVKeyboardSheet(text: $text)
}
}
}
UISearchController(行内键盘)
适用于搜索界面——所有输入都在一行内完成,但可自定义的内容非常有限:
let searchController = UISearchController(searchResultsController: resultsVC)
searchController.searchResultsUpdater = self
// Cannot customize text traits or add input accessories
SwiftUI .searchable()
SwiftUI 的 .searchable() 修饰符可在 tvOS 上使用,并会显示系统搜索键盘。标准搜索模式请使用此修饰符:
NavigationStack {
List(filteredItems) { item in
Text(item.title)
}
.searchable(text: $searchText, prompt: "Search movies")
}
对于 .searchable() 无法满足的自定义搜索 UI,请改用上述影子输入模式。
7. AVPlayer 调优
tvOS 媒体应用需要对 AVPlayer 进行特定配置,以获得良好的用户体验。
必要设置
let player = AVPlayer(url: streamURL)
// automaticallyWaitsToMinimizeStalling defaults to true (iOS 10+/tvOS 10+)
// Set false for immediate playback when synchronizing players
// or when you want playback to start ASAP from a non-empty buffer
player.automaticallyWaitsToMinimizeStalling = false
// Buffer hint — 0 means system chooses automatically
// Higher values reduce stalling risk but consume more memory
player.currentItem?.preferredForwardBufferDuration = 30
// Audio session — don't interrupt other apps' audio on launch
try AVAudioSession.sharedInstance().setCategory(.ambient)
// Switch to .playback when user presses play
自定义关闭逻辑
默认的向下滑动手势会关闭播放器。媒体应用可以重写此逻辑:
class PlayerViewController: AVPlayerViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Handle Menu button for custom back navigation
let menuPress = UITapGestureRecognizer(
target: self, action: #selector(handleMenu)
)
menuPress.allowedPressTypes = [
NSNumber(value: UIPress.PressType.menu.rawValue)
]
view.addGestureRecognizer(menuPress)
}
@objc func handleMenu() {
if isShowingControls {
hideControls()
} else {
dismiss(animated: true)
}
}
}
8. 菜单按钮状态机
Siri Remote 的菜单按钮既用于“返回”,也用于“关闭”。媒体应用需要使用状态机正确处理该按钮。
问题
State: Playing with controls visible
Menu press → Hide controls (not dismiss)
State: Playing with controls hidden
Menu press → Show "are you sure?" or dismiss
State: In submenu/settings overlay
Menu press → Close overlay (not dismiss player)
模式
enum PlayerState {
case loading // Buffering / loading content
case playing // Controls hidden
case controlsShown // Controls visible
case submenu // Settings/subtitles overlay
}
func handleMenuPress(in state: PlayerState) -> PlayerState {
switch state {
case .submenu:
dismissSubmenu()
return .controlsShown
case .controlsShown:
hideControls()
return .playing
case .playing:
dismiss(animated: true)
return .playing
case .loading:
cancelLoading()
dismiss(animated: true)
return .loading
}
}
9. 网络差异
IPv6 优先级
Apple TV 强烈优先使用 IPv6。所有 App Store 应用都必须支持仅 IPv6 网络(DNS64/NAT64)。如果后端仅支持 IPv4,连接速度可能较慢,或在某些网络中失败。
设备性能差异
| 设备 | 芯片 | 内存 | 备注 |
|---|---|---|---|
| Apple TV HD(第 4 代) | A8 | 2 GB | 仍受支持;速度慢得多 |
| Apple TV 4K(第 1 代) | A10X | 3 GB | 性能尚可 |
| Apple TV 4K(第 2 代) | A12 | 4 GB | 良好 |
| Apple TV 4K(第 3 代) | A15 | 4 GB | 出色 |
在较旧的硬件上测试。 Apple TV HD 仍在使用中,速度明显慢于 4K 机型。
10. 开发体验
仅用于调试的输入宏
在模拟器中使用键盘快捷键进行测试,无需 Siri Remote:
#if DEBUG
extension View {
func debugOnlyModifier() -> some View {
self.onKeyPress(.space) {
print("Space pressed — simulating select")
return .handled
}
}
}
#endif
视图检查辅助工具
#if DEBUG
extension View {
func debugBorder() -> some View {
border(.red, width: 1)
}
}
#endif
模拟器的局限性
- 模拟器无法准确模拟 Focus Engine 的行为
- 始终在真实的 Apple TV 设备上测试焦点导航
- 模拟器键盘输入 != Siri Remote 输入
- 必须在设备上进行性能分析(尤其是 Apple TV HD)
避免自我合理化
| 想法 | 现实 |
|---|---|
| “我直接使用与 iOS 相同的代码就行” | tvOS 在存储、焦点、输入和 Web 视图方面都与 iOS 不同。你会遇到各种障碍。 |
| “焦点的工作方式和 iOS 一样” | tvOS 采用双重焦点系统(UIKit Focus Engine + SwiftUI @FocusState)。仅使用 @FocusState 并不足够。 |
| “暂时用本地存储也没问题” | tvOS 上没有持久化本地存储。Apple 要求具备 iCloud 能力。 |
| “WebView 能正常工作” | Apple HIG:tvOS“不支持” Web 视图。只能使用 JavaScriptCore(没有 DOM)。 |
| “我用 TextField 处理文本输入就行” | UITextField 会触发全屏键盘。为获得更好的用户体验,可以考虑影子输入模式或 UISearchController。 |
| “我只需在模拟器上测试” | 焦点引擎和性能都需要在真实设备上测试。 |
资源
来源:“Surviving tvOS”(Ronnie Wong,2026)——Syncnext 媒体播放器的 tvOS 工程日志
Apple 文档:/tvuikit、/uikit/uifocusenvironment、/uikit/uifocusguide、/swiftui/focus、/gamecontroller/gcmicrogamepad、/avfoundation/avplayer、/javascriptcore
Apple 指南:tvOS 应用编程指南(存储、输入、手势)、HIG Web 视图(排除 tvOS)
WWDC:2016-215、2017-224、2021-10023、2021-10081、2021-10191、2023-10162、2025-219
技能:axiom-storage、axiom-sqlitedata、axiom-avfoundation-ref、axiom-hig-ref、axiom-liquid-glass