AI agent skill for Claude Code, Copilot, Cursor, Codex & Gemini — correct focus management for tvOS, iOS, macOS, visionOS & watchOS
See the code
Swift FocusEngine Pro is a free, open-source agent skill that helps AI coding assistants write correct focus management code for tvOS, iOS/iPadOS, watchOS, visionOS, and macOS. It covers SwiftUI, UIKit, AppKit, and RealityKit — targeting the mistakes LLMs actually make with Apple's focus engine.
Built from real-world experience shipping production tvOS apps, Apple developer documentation, WWDC sessions (2017-2025), and community best practices from Airbnb, Showmax, and others.
Works with Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, and any tool supporting the Agent Skills format.
Focus management on Apple platforms is one of the hardest things to get right — and one of the hardest things to debug when it breaks.
The focus engine is geometric, not hierarchical. It doesn't follow your view tree. When a user swipes right and focus jumps two rows away instead of to the next item, there's no error, no crash, no log — it just looks broken. The item wasn't perfectly vertically aligned, so the engine picked a different candidate. You'll spend hours in UIFocusDebugger before you figure out why.
Apple's documentation covers the APIs but not the real-world edge cases: what happens when you reload data and focus resets to the top, why .disabled() silently removes views from the focus chain on tvOS, why .focusSection() is the difference between a usable scroll view and chaos, or why onHover doesn't fire from eye gaze on visionOS.
LLMs generate focus code that compiles and looks reasonable — but breaks in ways you only discover on a real device with a Siri Remote in your hand. This skill is built from my experience getting focus to actually work in a complex, production tvOS app. Every anti-pattern in here is something I hit, debugged, and fixed.
# Global (all projects)
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro -g -y
# Project-level only
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro -y
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro --agent codex
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro --agent cursor
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro --agent github-copilot
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro --agent gemini
Any agent that supports the Agent Skills format can use this skill. See agentskills.io for instructions on adding skills to your agent.
Skills are installed as local copies — they don't auto-update. To pull the latest version:
# Update all installed skills
npx skills update -g -y
# Or reinstall this skill specifically
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro -g -y
⭐ Star and Watch this repo to get notified of new releases.
/swift-focusengine-pro Review this view for tvOS focus issues
$swift-focusengine-pro Check my SwiftUI code for focus anti-patterns
/swift-focusengine-pro Review this view for tvOS focus issues
/swift-focusengine-pro Review this view for tvOS focus issues
Use the swift-focusengine-pro skill to review my focus handling code
Use the Swift FocusEngine Pro skill to audit my project for focus management problems
| Reference | Platform | Coverage |
|---|---|---|
| anti-patterns.md | All | 30 critical mistakes: 17 original tvOS + 6 production tvOS + 7 macOS-specific |
| swiftui-focus.md | tvOS | @FocusState, focusSection, prefersDefaultFocus, AutoFocusManager pattern |
| uikit-focus.md | tvOS | UIFocusEnvironment, UIFocusGuide, shouldUpdateFocus, didUpdateFocus |
| ios-focus.md | iOS/iPadOS | SwiftUI + UIKit: focus groups, focusGroupIdentifier, UIFocusHaloEffect, keyboard nav, focusedValue, game controller, Stage Manager |
| watchos-focus.md | watchOS | SwiftUI: Digital Crown routing, sequential focus, Crown conflicts, .digitalCrownAccessory |
| visionos-focus.md | visionOS | SwiftUI + UIKit + RealityKit: gaze vs hover vs focus, HoverEffect, HoverEffectComponent |
| focus-styling.md | All | ButtonStyle + isFocused, FocusBorder, CABasicAnimation, CardButtonStyle, macOS focus ring styling |
| focus-restoration.md | All | Data reload handling, safe reload pattern, row offset tracking |
| layout-patterns.md | tvOS | Table-of-collections, sidebar+content, tab bar, hero+catalog |
| macos-focus.md | macOS | AppKit + SwiftUI: key view loop, focus ring, NSView focus APIs, focusedValue for menus, Mac Catalyst, Full Keyboard Access |
| realitykit-focus.md | visionOS | RealityKit entity hover, collision shapes, gestures, shader effects, mixed hierarchies |
| async-focus.md | All | @MainActor coordination, focus after data load, NavigationStack pop, Task cancellation |
| accessibility-focus.md | All | @AccessibilityFocusState, VoiceOver + focus, Full Keyboard Access, Switch Control, Reduce Motion |
| debugging.md | All | UIFocusDebugger, _whyIsThisViewNotFocusable, launch arguments, macOS first responder debugging |
.disabled() removes views from the focus chain on tvOS — gate the action inside the closure instead (.allowsHitTesting(false) is unreliable).focusSection() on horizontal ScrollViews — causes cross-row focus jumping in vertical layouts.focusable() to Buttons or NavigationLinks — creates double-focus artifactsreloadData() during animations — focus resets to the top of the screenframe.width in focus transform calculations — dimensions change when focusedsetNeedsFocusUpdate() called from wrong environment — silently fails with no errorisUserInteractionEnabled = false on headers/labels — removes them and their children from focus chainremembersLastFocusedIndexPath + offscreen reloadData() — remembered index may no longer existUIView.animate for CALayer properties — animations won't work, use CABasicAnimation@FocusState with focused(_:equals:) — can't represent "nothing focused" stateprepareForReuse() cleanup for focus state — stale focus styling on reused cellsprefersDefaultFocus inside ScrollView — may not work as expected, use defaultFocus insteadLazyVStack deallocates offscreen rows — rapid upward swipe causes focus to jump to tab bar, skipping content.focusSection() on vertical ScrollView — focus escapes upward to tab bar/nav bardidUpdateFocus/shouldUpdateFocus — per-frame garbage causes micro-stuttersacceptsFirstResponder on custom NSView — view is invisible to Tab navigationbecomeFirstResponder() directly — bypasses resign/become handshake, use window.makeFirstResponderbecomesKeyOnlyIfNeeded.focusable() on NSViewRepresentable — creates double focus layer conflicting with AppKitIn SwiftUI, use defaultFocus(_:_:) or prefersDefaultFocus. In UIKit, override preferredFocusEnvironments on the parent view controller. See swiftui-focus.md and uikit-focus.md.
Use remembersLastFocusedIndexPath or the safe reload pattern that locks focus before reloading. See focus-restoration.md.
The focus engine is geometric, not hierarchical. Add .focusSection() to horizontal ScrollViews to keep focus within rows. See anti-patterns.md (pattern #2).
You cannot directly set focus. Override preferredFocusEnvironments to return the target, then call setNeedsFocusUpdate() + updateFocusIfNeeded() on the correct focus environment. See uikit-focus.md.
Common causes: guide not added to the view hierarchy, preferredFocusEnvironments not set on the guide, or incorrect sizing/positioning. Focus guides bridge empty space between focusable views. See uikit-focus.md.
It creates a focus group that the engine treats as a contiguous region, preventing focus from skipping over the section to items in other rows. Essential for horizontal ScrollViews in vertical layouts. See swiftui-focus.md.
Use UIFocusDebugger.checkFocusability(for:) in the debugger, _whyIsThisViewNotFocusable on any UIView, and the UIFocusLoggingEnabled launch argument. See debugging.md.
On tvOS, .disabled() removes the view entirely from the focus chain. .allowsHitTesting(false) is commonly recommended but is unreliable — it may map to isUserInteractionEnabled = false under the hood. The most reliable approach is to gate the action inside the button closure instead of disabling the view. For lists/sidebars, use the dual @FocusState + .disabled() gating pattern (anti-pattern #25). See anti-patterns.md (patterns #1 and #25).
Setting @FocusState to nil should dismiss the keyboard, but it can fail inside sheets or NavigationStack. See ios-focus.md for workarounds.
Use @FocusState with an enum representing each field, then set the next case in onSubmit. See ios-focus.md.
iOS 15+ added UIFocusSystem support for hardware keyboards. Opt in with UIFocusHaloEffect, focusGroupIdentifier, and focusEffect. See ios-focus.md.
Each window scene has its own focus state. Use focusedSceneValue to propagate focus information across scenes. See ios-focus.md (Stage Manager section).
Define a FocusedValueKey, set values with .focusedSceneValue(), and read them with @FocusedValue in your menu bar or toolbar commands. See ios-focus.md.
The .digitalCrownRotation() modifier is order-sensitive. It must be applied in the correct position relative to other modifiers. See watchos-focus.md.
When a ScrollView contains a Digital Crown control, the Crown drives both scrolling and the control. Use explicit @FocusState to determine which element owns the Crown. See watchos-focus.md.
visionOS uses eye tracking for hover (.hoverEffect()) and indirect input for focus. They are separate systems. Gaze creates hover highlights, but tap gestures are needed for activation. See visionos-focus.md.
Use HoverEffectComponent on RealityKit entities with styles: default, spotlight, shader, or highlight. For SwiftUI views, use .hoverEffect(.highlight) or .hoverEffect(.lift). See realitykit-focus.md.
Mac Catalyst inherits iPad's UIFocusSystem — UIFocusHaloEffect renders as a macOS focus ring, and focusGroupIdentifier maps to Tab navigation groups. If your iPad app doesn't support keyboard focus, neither will the Catalyst version. See macos-focus.md (Mac Catalyst section).
Use @FocusState (same as iOS) and .focusable() for custom views. macOS focus is always active — no hardware keyboard requirement. For menu bar integration, use focusedValue / focusedSceneValue. See macos-focus.md.
The focus engine behaves differently between Xcode Simulator and physical hardware, especially for tvOS remote gestures and visionOS eye tracking. Always test focus on real devices. See debugging.md.
Built from:
Swift FocusEngine Pro pairs well with these skills:
See the Swift Agent Skills directory for more.
See CHANGELOG.md for release history.
Contributions are welcome! Focus on:
Keep reference files focused and under 300 lines each. Don't repeat things LLMs already know — focus on what they get wrong. All contributions must be MIT licensed.
Please read the Code of Conduct before contributing.
Swift FocusEngine Pro was created by Michael Haviv and is licensed under the MIT License.
AI agent skill for Claude Code, Copilot, Cursor, Codex & Gemini — correct focus management for tvOS, iOS, macOS, visionOS & watchOS
See the code
Swift FocusEngine Pro is a free, open-source agent skill that helps AI coding assistants write correct focus management code for tvOS, iOS/iPadOS, watchOS, visionOS, and macOS. It covers SwiftUI, UIKit, AppKit, and RealityKit — targeting the mistakes LLMs actually make with Apple's focus engine.
Built from real-world experience shipping production tvOS apps, Apple developer documentation, WWDC sessions (2017-2025), and community best practices from Airbnb, Showmax, and others.
Works with Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, and any tool supporting the Agent Skills format.
Focus management on Apple platforms is one of the hardest things to get right — and one of the hardest things to debug when it breaks.
The focus engine is geometric, not hierarchical. It doesn't follow your view tree. When a user swipes right and focus jumps two rows away instead of to the next item, there's no error, no crash, no log — it just looks broken. The item wasn't perfectly vertically aligned, so the engine picked a different candidate. You'll spend hours in UIFocusDebugger before you figure out why.
Apple's documentation covers the APIs but not the real-world edge cases: what happens when you reload data and focus resets to the top, why .disabled() silently removes views from the focus chain on tvOS, why .focusSection() is the difference between a usable scroll view and chaos, or why onHover doesn't fire from eye gaze on visionOS.
LLMs generate focus code that compiles and looks reasonable — but breaks in ways you only discover on a real device with a Siri Remote in your hand. This skill is built from my experience getting focus to actually work in a complex, production tvOS app. Every anti-pattern in here is something I hit, debugged, and fixed.
# Global (all projects)
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro -g -y
# Project-level only
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro -y
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro --agent codex
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro --agent cursor
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro --agent github-copilot
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro --agent gemini
Any agent that supports the Agent Skills format can use this skill. See agentskills.io for instructions on adding skills to your agent.
Skills are installed as local copies — they don't auto-update. To pull the latest version:
# Update all installed skills
npx skills update -g -y
# Or reinstall this skill specifically
npx skills add https://github.com/mhaviv/Swift-FocusEngine-Agent-Skill --skill swift-focusengine-pro -g -y
⭐ Star and Watch this repo to get notified of new releases.
/swift-focusengine-pro Review this view for tvOS focus issues
$swift-focusengine-pro Check my SwiftUI code for focus anti-patterns
/swift-focusengine-pro Review this view for tvOS focus issues
/swift-focusengine-pro Review this view for tvOS focus issues
Use the swift-focusengine-pro skill to review my focus handling code
Use the Swift FocusEngine Pro skill to audit my project for focus management problems
| Reference | Platform | Coverage |
|---|---|---|
| anti-patterns.md | All | 30 critical mistakes: 17 original tvOS + 6 production tvOS + 7 macOS-specific |
| swiftui-focus.md | tvOS | @FocusState, focusSection, prefersDefaultFocus, AutoFocusManager pattern |
| uikit-focus.md | tvOS | UIFocusEnvironment, UIFocusGuide, shouldUpdateFocus, didUpdateFocus |
| ios-focus.md | iOS/iPadOS | SwiftUI + UIKit: focus groups, focusGroupIdentifier, UIFocusHaloEffect, keyboard nav, focusedValue, game controller, Stage Manager |
| watchos-focus.md | watchOS | SwiftUI: Digital Crown routing, sequential focus, Crown conflicts, .digitalCrownAccessory |
| visionos-focus.md | visionOS | SwiftUI + UIKit + RealityKit: gaze vs hover vs focus, HoverEffect, HoverEffectComponent |
| focus-styling.md | All | ButtonStyle + isFocused, FocusBorder, CABasicAnimation, CardButtonStyle, macOS focus ring styling |
| focus-restoration.md | All | Data reload handling, safe reload pattern, row offset tracking |
| layout-patterns.md | tvOS | Table-of-collections, sidebar+content, tab bar, hero+catalog |
| macos-focus.md | macOS | AppKit + SwiftUI: key view loop, focus ring, NSView focus APIs, focusedValue for menus, Mac Catalyst, Full Keyboard Access |
| realitykit-focus.md | visionOS | RealityKit entity hover, collision shapes, gestures, shader effects, mixed hierarchies |
| async-focus.md | All | @MainActor coordination, focus after data load, NavigationStack pop, Task cancellation |
| accessibility-focus.md | All | @AccessibilityFocusState, VoiceOver + focus, Full Keyboard Access, Switch Control, Reduce Motion |
| debugging.md | All | UIFocusDebugger, _whyIsThisViewNotFocusable, launch arguments, macOS first responder debugging |
.disabled() removes views from the focus chain on tvOS — gate the action inside the closure instead (.allowsHitTesting(false) is unreliable).focusSection() on horizontal ScrollViews — causes cross-row focus jumping in vertical layouts.focusable() to Buttons or NavigationLinks — creates double-focus artifactsreloadData() during animations — focus resets to the top of the screenframe.width in focus transform calculations — dimensions change when focusedsetNeedsFocusUpdate() called from wrong environment — silently fails with no errorisUserInteractionEnabled = false on headers/labels — removes them and their children from focus chainremembersLastFocusedIndexPath + offscreen reloadData() — remembered index may no longer existUIView.animate for CALayer properties — animations won't work, use CABasicAnimation@FocusState with focused(_:equals:) — can't represent "nothing focused" stateprepareForReuse() cleanup for focus state — stale focus styling on reused cellsprefersDefaultFocus inside ScrollView — may not work as expected, use defaultFocus insteadLazyVStack deallocates offscreen rows — rapid upward swipe causes focus to jump to tab bar, skipping content.focusSection() on vertical ScrollView — focus escapes upward to tab bar/nav bardidUpdateFocus/shouldUpdateFocus — per-frame garbage causes micro-stuttersacceptsFirstResponder on custom NSView — view is invisible to Tab navigationbecomeFirstResponder() directly — bypasses resign/become handshake, use window.makeFirstResponderbecomesKeyOnlyIfNeeded.focusable() on NSViewRepresentable — creates double focus layer conflicting with AppKitIn SwiftUI, use defaultFocus(_:_:) or prefersDefaultFocus. In UIKit, override preferredFocusEnvironments on the parent view controller. See swiftui-focus.md and uikit-focus.md.
Use remembersLastFocusedIndexPath or the safe reload pattern that locks focus before reloading. See focus-restoration.md.
The focus engine is geometric, not hierarchical. Add .focusSection() to horizontal ScrollViews to keep focus within rows. See anti-patterns.md (pattern #2).
You cannot directly set focus. Override preferredFocusEnvironments to return the target, then call setNeedsFocusUpdate() + updateFocusIfNeeded() on the correct focus environment. See uikit-focus.md.
Common causes: guide not added to the view hierarchy, preferredFocusEnvironments not set on the guide, or incorrect sizing/positioning. Focus guides bridge empty space between focusable views. See uikit-focus.md.
It creates a focus group that the engine treats as a contiguous region, preventing focus from skipping over the section to items in other rows. Essential for horizontal ScrollViews in vertical layouts. See swiftui-focus.md.
Use UIFocusDebugger.checkFocusability(for:) in the debugger, _whyIsThisViewNotFocusable on any UIView, and the UIFocusLoggingEnabled launch argument. See debugging.md.
On tvOS, .disabled() removes the view entirely from the focus chain. .allowsHitTesting(false) is commonly recommended but is unreliable — it may map to isUserInteractionEnabled = false under the hood. The most reliable approach is to gate the action inside the button closure instead of disabling the view. For lists/sidebars, use the dual @FocusState + .disabled() gating pattern (anti-pattern #25). See anti-patterns.md (patterns #1 and #25).
Setting @FocusState to nil should dismiss the keyboard, but it can fail inside sheets or NavigationStack. See ios-focus.md for workarounds.
Use @FocusState with an enum representing each field, then set the next case in onSubmit. See ios-focus.md.
iOS 15+ added UIFocusSystem support for hardware keyboards. Opt in with UIFocusHaloEffect, focusGroupIdentifier, and focusEffect. See ios-focus.md.
Each window scene has its own focus state. Use focusedSceneValue to propagate focus information across scenes. See ios-focus.md (Stage Manager section).
Define a FocusedValueKey, set values with .focusedSceneValue(), and read them with @FocusedValue in your menu bar or toolbar commands. See ios-focus.md.
The .digitalCrownRotation() modifier is order-sensitive. It must be applied in the correct position relative to other modifiers. See watchos-focus.md.
When a ScrollView contains a Digital Crown control, the Crown drives both scrolling and the control. Use explicit @FocusState to determine which element owns the Crown. See watchos-focus.md.
visionOS uses eye tracking for hover (.hoverEffect()) and indirect input for focus. They are separate systems. Gaze creates hover highlights, but tap gestures are needed for activation. See visionos-focus.md.
Use HoverEffectComponent on RealityKit entities with styles: default, spotlight, shader, or highlight. For SwiftUI views, use .hoverEffect(.highlight) or .hoverEffect(.lift). See realitykit-focus.md.
Mac Catalyst inherits iPad's UIFocusSystem — UIFocusHaloEffect renders as a macOS focus ring, and focusGroupIdentifier maps to Tab navigation groups. If your iPad app doesn't support keyboard focus, neither will the Catalyst version. See macos-focus.md (Mac Catalyst section).
Use @FocusState (same as iOS) and .focusable() for custom views. macOS focus is always active — no hardware keyboard requirement. For menu bar integration, use focusedValue / focusedSceneValue. See macos-focus.md.
The focus engine behaves differently between Xcode Simulator and physical hardware, especially for tvOS remote gestures and visionOS eye tracking. Always test focus on real devices. See debugging.md.
Built from:
Swift FocusEngine Pro pairs well with these skills:
See the Swift Agent Skills directory for more.
See CHANGELOG.md for release history.
Contributions are welcome! Focus on:
Keep reference files focused and under 300 lines each. Don't repeat things LLMs already know — focus on what they get wrong. All contributions must be MIT licensed.
Please read the Code of Conduct before contributing.
Swift FocusEngine Pro was created by Michael Haviv and is licensed under the MIT License.