mcpField
A
axiom-tvos

axiom-tvos

github.com/charleswiltgen开发1.2k96 次安装今天更新
代码仓库

关于 axiom-tvos

供开发者构建或移植 tvOS 应用的参考技能,涵盖焦点、Siri Remote 输入、存储和网页内容等平台差异,并指出常见运行时问题。

主要功能

  1. 01UIKit 与 SwiftUI 焦点系统
  2. 02Siri Remote 输入处理
  3. 03tvOS 存储限制
  4. 04网页内容替代方案
  5. 05文本输入和 AVPlayer 注意事项

适用场景

  1. 01将 iOS 应用移植到 tvOS
  2. 02排查焦点导航问题
  3. 03规划应用数据持久化
  4. 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 代)A82 GB仍受支持;速度慢得多
Apple TV 4K(第 1 代)A10X3 GB性能尚可
Apple TV 4K(第 2 代)A124 GB良好
Apple TV 4K(第 3 代)A154 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

评论

支持 Markdown