Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions tvos-focus/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <repo-path>` — 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 <repo-path>` 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.
Expand Down
77 changes: 77 additions & 0 deletions tvos-focus/scripts/audit-tvos-focus.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/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

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 ""

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)"
Comment on lines +29 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate repo path before suppressing ripgrep errors

When REPO is invalid or unreadable, this command hides ripgrep’s error output (2>/dev/null) and falls back to (none found), so the script exits successfully with a misleading “clean” audit. Running audit-tvos-focus.sh /tmp/does-not-exist currently reports no findings instead of failing, which can mask operator mistakes and invalidate audit results; add an upfront path check (and/or treat rg exit code 2 as an error) before printing empty-category output.

Useful? React with 👍 / 👎.

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\s*[\(\{]|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\s*[\(\{]|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 ==="