From 0491e97bf588127725510cc14339087352882d27 Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Thu, 30 Apr 2026 21:10:47 +0300 Subject: [PATCH 1/3] feat: add tvos focus audit script --- tvos-focus/SKILL.md | 9 ++++ tvos-focus/scripts/audit-tvos-focus.sh | 67 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100755 tvos-focus/scripts/audit-tvos-focus.sh diff --git a/tvos-focus/SKILL.md b/tvos-focus/SKILL.md index 09620c1..bf211f0 100644 --- a/tvos-focus/SKILL.md +++ b/tvos-focus/SKILL.md @@ -7,6 +7,15 @@ description: This skill should be used when the user asks about "tvOS focus", "A tvOS focus is spatial — the engine casts a ray from the focused element and picks the nearest focusable rectangle in the swipe direction. This is the ONLY interaction model on Apple TV. No touch, no cursor. +## Available Resources + +- `scripts/audit-tvos-focus.sh ` — run first to find SwiftUI focus APIs, default chrome hotspots, invisible controls, Spacer gaps, ScrollView traps, directional command handlers, overlays, and focus restore code. +- `references/full-guide.md` — complete tvOS focus guide with detailed examples and testing approaches. + +## Audit First + +Run `scripts/audit-tvos-focus.sh ` before editing focus code. Treat the output as review leads, not automatic bugs. Prioritize matches in this order: invisible or clear interactive views, missing custom `ButtonStyle`, `Spacer()` gaps between interactive sections, ScrollViews without focus grouping, overlays without background disabling, and missing focus restore after dismissal. + ## Gotchas (the stuff Claude gets wrong) These are real failures from production tvOS apps. Check these FIRST before writing focus code. diff --git a/tvos-focus/scripts/audit-tvos-focus.sh b/tvos-focus/scripts/audit-tvos-focus.sh new file mode 100755 index 0000000..f3b2a7f --- /dev/null +++ b/tvos-focus/scripts/audit-tvos-focus.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# Audit a SwiftUI tvOS app for common focus navigation risk areas. +# Usage: audit-tvos-focus.sh [path-to-repo] +# Outputs a structured report of findings grouped by category. + +set -o pipefail + +REPO="${1:-.}" + +if ! command -v rg >/dev/null 2>&1; then + echo "error: ripgrep (rg) is required for this audit" >&2 + exit 1 +fi + +echo "=== tvOS Focus Audit: $REPO ===" +echo "" + +echo "## 1. Focus APIs In Use" +rg -n 'FocusState|@FocusState|\.focused\(|\.focusSection\(|\.prefersDefaultFocus|\.defaultFocus|\.focusScope|\.focusEffect|\.focusable\(' \ + --type swift "$REPO" 2>/dev/null || echo " (none found)" +echo "" + +echo "## 2. Button Styles And Default Focus Chrome Hotspots" +rg -n 'ButtonStyle|\.buttonStyle\(\.plain\)|\.buttonStyle\(|@Environment\(\\\.isFocused\)|isFocused' \ + --type swift "$REPO" 2>/dev/null | head -120 || echo " (none found)" +echo "" + +echo "## 3. Invisible Or Clear Interactive Views" +rg -n 'Button\s*\{|Color\.clear|\.opacity\(0\)|\.hidden\(\)|\.frame\(width:\s*1|\.frame\(.*height:\s*1' \ + --type swift "$REPO" 2>/dev/null | head -120 || echo " (none found)" +echo "" + +echo "## 4. Spacer Gaps That May Break Spatial Navigation" +rg -n 'Spacer\(\)' --type swift "$REPO" 2>/dev/null | head -80 || echo " (none found)" +echo "" + +echo "## 5. ScrollViews That May Trap Directional Input" +rg -n 'ScrollView\(|LazyHStack|LazyVStack|\.scrollTarget|\.scrollPosition' \ + --type swift "$REPO" 2>/dev/null | head -120 || echo " (none found)" +echo "" + +echo "## 6. Directional Command Handlers" +rg -n '\.onMoveCommand|\.onExitCommand|\.onPlayPauseCommand|MoveCommandDirection|pressesBegan|UIPress' \ + --type swift "$REPO" 2>/dev/null || echo " (none found)" +echo "" + +echo "## 7. Overlay And Modal Focus Capture" +rg -n 'ZStack|overlay\(|fullScreenCover|sheet\(|popover\(|\.disabled\(|isPresented|isOverlay|showOverlay|dismiss' \ + --type swift "$REPO" 2>/dev/null | head -160 || echo " (none found)" +echo "" + +echo "## 8. Focus Restore After State Changes" +rg -n '\.onChange\(of:|DispatchQueue\.main\.async|Task\s*\{|focused[A-Za-z0-9_]*\s*=' \ + --type swift "$REPO" 2>/dev/null | head -120 || echo " (none found)" +echo "" + +echo "## 9. tvOS-Specific Files And Targets" +TVOS_FILES="$(find "$REPO" \( -name '*tvOS*' -o -name '*.xcodeproj' -o -name 'Package.swift' \) \ + -not -path '*/.*' 2>/dev/null)" +if [ -n "$TVOS_FILES" ]; then + echo "$TVOS_FILES" +else + echo " (none found)" +fi +echo "" + +echo "=== End Audit ===" From a1d55a80bb0b9004a6d382f180a06bddefe45cb1 Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Mon, 15 Jun 2026 13:26:40 +0300 Subject: [PATCH 2/3] fix: validate tvos audit repository path --- tvos-focus/scripts/audit-tvos-focus.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tvos-focus/scripts/audit-tvos-focus.sh b/tvos-focus/scripts/audit-tvos-focus.sh index f3b2a7f..1a0de81 100755 --- a/tvos-focus/scripts/audit-tvos-focus.sh +++ b/tvos-focus/scripts/audit-tvos-focus.sh @@ -12,6 +12,16 @@ if ! command -v rg >/dev/null 2>&1; then exit 1 fi +if [ ! -d "$REPO" ]; then + echo "error: repository path is not a directory: $REPO" >&2 + exit 1 +fi + +if [ ! -r "$REPO" ]; then + echo "error: repository path is not readable: $REPO" >&2 + exit 1 +fi + echo "=== tvOS Focus Audit: $REPO ===" echo "" From e426252bf8a675a517394044b2f8b716361fea45 Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Mon, 15 Jun 2026 16:15:00 +0300 Subject: [PATCH 3/3] fix: detect trailing-closure focus hotspots --- tvos-focus/scripts/audit-tvos-focus.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tvos-focus/scripts/audit-tvos-focus.sh b/tvos-focus/scripts/audit-tvos-focus.sh index 1a0de81..0a41971 100755 --- a/tvos-focus/scripts/audit-tvos-focus.sh +++ b/tvos-focus/scripts/audit-tvos-focus.sh @@ -45,7 +45,7 @@ rg -n 'Spacer\(\)' --type swift "$REPO" 2>/dev/null | head -80 || echo " (none echo "" echo "## 5. ScrollViews That May Trap Directional Input" -rg -n 'ScrollView\(|LazyHStack|LazyVStack|\.scrollTarget|\.scrollPosition' \ +rg -n 'ScrollView\s*[\(\{]|LazyHStack|LazyVStack|\.scrollTarget|\.scrollPosition' \ --type swift "$REPO" 2>/dev/null | head -120 || echo " (none found)" echo "" @@ -55,7 +55,7 @@ rg -n '\.onMoveCommand|\.onExitCommand|\.onPlayPauseCommand|MoveCommandDirection echo "" echo "## 7. Overlay And Modal Focus Capture" -rg -n 'ZStack|overlay\(|fullScreenCover|sheet\(|popover\(|\.disabled\(|isPresented|isOverlay|showOverlay|dismiss' \ +rg -n 'ZStack|\.overlay\s*[\(\{]|fullScreenCover|sheet\(|popover\(|\.disabled\(|isPresented|isOverlay|showOverlay|dismiss' \ --type swift "$REPO" 2>/dev/null | head -160 || echo " (none found)" echo ""