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. 01iOS アプリを tvOS に移植
  2. 02フォーカス移動の問題を調査
  3. 03アプリデータの永続化を設計
  4. 04WebView ベースのコンテンツを置き換え

インストール

コマンドをコピーしてターミナルで実行します。

$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 のように機能する、という3つです。

基本原則 tvOS は「テレビ上の iOS」ではありません。デュアルフォーカスシステムがあり、永続的なローカルストレージはなく、WebView もありません。また、互換性のない2世代のリモコンがあります。独自のプラットフォームとして扱ってください。

tvOS 26 では Liquid Glass のデザイン言語と、新しいアプリアイコンシステムが採用されています。実装パターンについては axiom-liquid-glass を参照してください。

tvOS 移植時のトリアージ

tvOS への移植をリリースする前に、次の5つの領域を確認してください。tvOS 固有のバグの90%は、これらに起因します。

領域確認事項セクション
ストレージ永続的なローカルファイルは不可。iCloud が必要§3
フォーカスデュアルシステムが動作していること。隙間はフォーカスガイドで補完§1
WebViewJavaScriptCore またはネイティブ描画に置き換えられていること§4
テキスト入力シャドウ入力またはフルスクリーンキーボードが処理されていること§6
AVPlayerオーディオセッション、バッファー、Menu ボタンのステートマシン§7、§8

「tvOS でコンパイルできる」ことには何の意味もありません。この5つの領域はコンパイルに成功しても、実行時に失敗します。

このスキルを使う場面

  • 新しい tvOS アプリを開発する、または tvOS ターゲットを追加する
  • iOS アプリを tvOS に移植する
  • tvOS のフォーカス、リモート入力、ストレージに関する問題をデバッグする
  • tvOS で AVPlayer、TVUIKit、テキスト入力を扱う

プロンプトの例

開発者が実際に尋ねる質問と、このスキルが答える内容を紹介します。

1. 「iOS アプリを tvOS に移植していますが、フォーカスナビゲーションが動作しません」

-> このスキルでは、デュアルフォーカスシステム(UIKit Focus Engine と @FocusState)とよくある落とし穴について説明します

2. 「tvOS アプリを起動するたびに、すべてのデータが失われます」

-> このスキルでは、永続的なローカルストレージがないことを説明し、iCloud を優先するパターンを紹介します

3. 「tvOS の SwiftUI で Siri Remote の入力を処理するにはどうすればよいですか?」

-> このスキルでは、両世代のリモコンと3つの入力レイヤー(SwiftUI、UIKit ジェスチャー、GameController)を扱います

4. 「tvOS で WebView が動作しません。Web コンテンツを表示するにはどうすればよいですか?」

-> このスキルでは、解析に 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 には、共存させる必要がある2つのフォーカスシステムがあります。これは iOS 開発者が最も混乱する原因です。

デュアルシステム
システム制御対象API
UIKit Focus Engineハードウェアリモコンによるナビゲーション、方向スキャンUIFocusEnvironment、UIFocusSystem、UIFocusGuide
SwiftUI フォーカスプログラムによるフォーカスのバインディング、フォーカスセクション@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 Focus 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 の入力

ハードウェアが異なる 2 世代があるため、コードで両方に対応する必要があります。

世代ごとの違い
機能第 1 世代(2015-2021)第 2 世代(2021 以降)
上面タッチパッド(全面スワイプ)クリックパッド + 外周のタッチリング
スワイプジェスチャ全面リングの縁のみ
クリック操作中央を押す十字キー形式
加速度計ありあり
標準 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
Game Controller フレームワーク(生入力)

カスタム操作(シーク、ゲーム)では、Siri Remote に GCMicroGamepad としてアクセスします。

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 の App Programming Guide for tvOS より:「新しい Apple TV 向けに開発されるすべてのアプリは、iCloud にデータを保存し、優れたユーザー体験を提供する方法でデータを取得できなければなりません。」

tvOS にあるもの
ディレクトリ存在するか永続性
Documentsいいえ該当なし
Application Supportはいいいえ — アプリが実行されていない間にシステムが削除する可能性があります
Cachesはいいいえ — ストレージ不足時にシステムが削除します
tmpはいいいえ
サイズ制限
  • アプリバンドル:最大 4 GB
  • NSUserDefaults / UserDefaults:最大 500 KB(Apple のドキュメントによる。iOS の 4 MB ではありません)。利用可能ですが、システムによる消去の対象となるため、セッション間で永続する保証はありません
  • オンデマンドリソース:OS が管理する読み取り専用アセットに利用できます
  • ローカルキャッシュ:サイズの保証はありません。アプリが実行されていない間にシステムが消去する可能性があります
これが意味すること
  • ローカルファイルはすべて、アプリの起動間に消える可能性があります
  • ローカルに保存された 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動作しますが、不安定ですローカルのみの永続ストレージはありません。ModelContainer は最初から CloudKit 用に構成する必要があり、後から同期を追加するには移行が必要です。システムによってデータベースが削除されると、次回起動時に完全な再同期が行われます
CoreData + CloudKit危険ですCloudKit のメタデータによって容量が膨らみます
ローカルのみの GRDB/SQLite信頼性に欠けますシステムによってデータベースファイルが削除されます
NSUbiquitousKeyValueStore小規模なデータに適しています1 MB の上限があり、キーと値の形式のみです
オンデマンドリソース読み取り専用のアセットに適していますダウンロードと削除のライフサイクルは OS が管理します

参照先:CloudKit SyncEngine のパターンについては axiom-sqlitedata、ストレージの選択肢の全体については axiom-storage をご覧ください。


4. WebView は使用できません

tvOS には WKWebView、SFSafariViewController、WebView がありません。Apple の HIG では、Web ビューは「tvOS ではサポートされていません」と明記されています。

できること
必要なことソリューション
HTML/JSON の解析JavaScriptCore(JSContext、JSValue — DOM はありません)を使用します
Web コンテンツの表示解析したデータからネイティブに描画します
m3u8 からの HLS ストリーミングローカル HTTP サーバーのパターン(以下を参照)
OAuth ログインデバイスコードフロー(RFC 8628)またはコンパニオンデバイス
解析に JavaScriptCore を使用する

JavaScriptCore は、DOM や Web 描画機能を持たない JavaScript 実行エンジンを提供します。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画像、テキスト、方向性のある視差効果を備えたボタン
TVMonogramViewPersonNameComponents を使ったユーザーのイニシャルまたは画像
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 でのテキスト入力を最小限に抑えることを推奨しています。

3 つの方法
方法最適な用途キーボードのスタイル
UIAlertController手早く簡単な入力テキストフィールド付きモーダル
UITextField複数フィールドのフォーム「次へ」/「前へ」付きの全画面キーボード
UISearchController検索インラインの 1 行キーボード
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(インラインキーボード)

検索インターフェース向けです。入力はすべて 1 行に収まりますが、カスタマイズできる範囲は非常に限られています:

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 のメディアアプリで快適な UX を実現するには、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 のみに対応している場合、一部のネットワークでは接続が遅くなったり、失敗したりすることがあります。

デバイスのパフォーマンス差
デバイスチップRAM備考
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
シミュレータの制限事項
  • シミュレータでは、フォーカスエンジンの動作が正確に再現されません
  • フォーカス移動は、必ず実機の Apple TV でテストしてください
  • シミュレータのキーボード入力と Siri Remote の入力は異なります
  • パフォーマンスのプロファイリングは実機で行う必要があります(特に Apple TV HD)

不合理な思い込みへの対処

思い込み実際のところ
「iOS と同じコードを使えばいい」tvOS はストレージ、フォーカス、入力、Web ビューの点で iOS と異なります。問題に直面することになります。
「フォーカスの動作は iOS と同じ」tvOS には、UIKit フォーカスエンジンと SwiftUI の @FocusState という、2つのフォーカスシステムがあります。@FocusState だけでは不十分です。
「とりあえずローカルストレージで問題ない」tvOS には永続的なローカルストレージがありません。Apple は iCloud 機能を必須としています。
「WebView は使える」Apple の HIG では、Web ビューは「tvOS ではサポートされていません」。DOM はなく、JavaScriptCore のみです。
「TextField でテキスト入力を処理すればいい」UITextField を使うと、フルスクリーンキーボードが表示されます。UX を向上させるには、シャドー入力パターンや UISearchController の使用を検討してください。
「シミュレータでテストするだけでいい」フォーカスエンジンとパフォーマンスの検証には、実機でのテストが必要です。

リソース

出典:「Surviving tvOS」(Ronnie Wong、2026年)— Syncnext メディアプレーヤーの tvOS エンジニアリングログ

Apple のドキュメント:/tvuikit、/uikit/uifocusenvironment、/uikit/uifocusguide、/swiftui/focus、/gamecontroller/gcmicrogamepad、/avfoundation/avplayer、/javascriptcore

Apple のガイド:App Programming Guide for tvOS(ストレージ、入力、ジェスチャ)、HIG Web Views(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

コメント