{"id":"2562bcbc-6c2d-44f8-bb3d-fb0436cb29ba","shortId":"hpLRNv","kind":"skill","title":"focus-engine","tagline":"Implements keyboard, directional, and scene-level focus behavior across SwiftUI and UIKit. Use when managing @FocusState, defaultFocus, focused values, focusable interactions, focus sections, tvOS geometric focus model and Siri Remote navigation, watchOS Digital Crown focus, visi","description":"# Focus Engine\n\nFocus behavior for SwiftUI and UIKit apps targeting iOS 26+, iPadOS, macOS, and tvOS. Covers keyboard focus, directional focus, scene-focused values, focus restoration, and UIKit focus guides. `focusSection()` guidance in this skill applies to macOS and tvOS. Accessibility-specific focus for VoiceOver and Switch Control lives in the `ios-accessibility` skill.\n\n## Contents\n\n- [SwiftUI FocusState](#swiftui-focusstate)\n- [Default Focus](#default-focus)\n- [Focused Values and Scene Values](#focused-values-and-scene-values)\n- [Focusable Interactions](#focusable-interactions)\n- [Focus Sections](#focus-sections)\n- [Focus Restoration](#focus-restoration)\n- [UIKit Focus Guides](#uikit-focus-guides)\n- [Common Mistakes](#common-mistakes)\n- [Review Checklist](#review-checklist)\n- [References](#references)\n\n## SwiftUI FocusState\n\nUse `@FocusState` to read and write focus placement inside a scene. Use `Bool` for a single target or an optional `Hashable` enum for multiple targets.\n\n```swift\nstruct LoginView: View {\n    enum Field: Hashable { case email, password }\n\n    @State private var email = \"\"\n    @State private var password = \"\"\n    @FocusState private var focusedField: Field?\n\n    var body: some View {\n        Form {\n            TextField(\"Email\", text: $email)\n                .focused($focusedField, equals: .email)\n\n            SecureField(\"Password\", text: $password)\n                .focused($focusedField, equals: .password)\n        }\n        .onAppear { focusedField = .email }\n        .onSubmit {\n            switch focusedField {\n            case .email: focusedField = .password\n            case .password, nil: submit()\n            }\n        }\n    }\n}\n```\n\nKeep focus state local to the view that owns the focusable controls.\n\n## Default Focus\n\nUse `.defaultFocus` to set the preferred initial focus region or control when a view appears or when focus is reassigned automatically.\n\n```swift\nstruct SidebarView: View {\n    enum Target: Hashable { case library, settings }\n    @FocusState private var focusedTarget: Target?\n\n    var body: some View {\n        VStack {\n            Button(\"Library\") { }\n                .focused($focusedTarget, equals: .library)\n\n            Button(\"Settings\") { }\n                .focused($focusedTarget, equals: .settings)\n        }\n        .defaultFocus($focusedTarget, .library)\n    }\n}\n```\n\nPrefer one clear default destination per screen or focus region.\n\n## Focused Values and Scene Values\n\nUse focused values to expose state from the currently focused view. Use scene-focused values when commands or scene-wide UI should keep access to the value even after focus moves within that scene.\n\n```swift\nstruct SelectedRecipeKey: FocusedValueKey {\n    typealias Value = Binding<Recipe>\n}\n\nextension FocusedValues {\n    var selectedRecipe: Binding<Recipe>? {\n        get { self[SelectedRecipeKey.self] }\n        set { self[SelectedRecipeKey.self] = newValue }\n    }\n}\n\nstruct RecipeDetailView: View {\n    @Binding var recipe: Recipe\n\n    var body: some View {\n        Text(recipe.title)\n            .focusedSceneValue(\\.selectedRecipe, $recipe)\n    }\n}\n```\n\nUse this pattern for menus, commands, and toolbars that need to act on the focused scene's current content.\n\n## Focusable Interactions\n\nUse `.focusable(_:interactions:)` on custom SwiftUI views that should participate in keyboard or directional focus.\n\n```swift\nstruct SelectableCard: View {\n    let title: String\n    let action: () -> Void\n    @FocusState private var isFocused: Bool\n\n    var body: some View {\n        Button(action: action) {\n            RoundedRectangle(cornerRadius: 12)\n                .fill(isFocused ? Color.accentColor.opacity(0.15) : .clear)\n                .overlay { Text(title) }\n        }\n        .buttonStyle(.plain)\n        .focusable(interactions: .activate)\n        .focused($isFocused)\n    }\n}\n```\n\nUse `.activate` for button-like controls. Reserve broader interactions for views that genuinely need editing or multiple focus-driven behaviors.\n\n## Focus Sections\n\nUse `focusSection()` on macOS 13+ and tvOS 15+ to guide directional movement across groups of focusable descendants in uneven layouts.\n\n```swift\nstruct TVLibraryView: View {\n    var body: some View {\n        HStack {\n            VStack {\n                Button(\"Recent\") { }\n                Button(\"Favorites\") { }\n                Button(\"Downloaded\") { }\n            }\n            .focusSection()\n\n            VStack {\n                Button(\"Featured\") { }\n                Button(\"Top Picks\") { }\n                Button(\"Continue Watching\") { }\n            }\n            .focusSection()\n        }\n    }\n}\n```\n\nUse focus sections on macOS and tvOS when default left/right or up/down movement skips the intended group.\n\n## Focus Restoration\n\nAfter dismissing a sheet, popover, or transient overlay, return focus to a stable trigger or logical next target.\n\n```swift\nstruct FiltersView: View {\n    @State private var showSheet = false\n    @FocusState private var isFilterButtonFocused: Bool\n\n    var body: some View {\n        Button(\"Filters\") { showSheet = true }\n            .focused($isFilterButtonFocused)\n            .sheet(isPresented: $showSheet) {\n                FilterEditor()\n                    .onDisappear {\n                        Task { @MainActor in\n                            isFilterButtonFocused = true\n                        }\n                    }\n            }\n    }\n}\n```\n\nRestore focus intentionally whenever presentation changes would otherwise leave users disoriented.\n\n## UIKit Focus Guides\n\nUse `UIFocusGuide` when UIKit or tvOS layouts need custom routing across empty space or awkward geometry.\n\n```swift\nfinal class DashboardViewController: UIViewController {\n    private let focusGuide = UIFocusGuide()\n    @IBOutlet private weak var leadingButton: UIButton!\n    @IBOutlet private weak var trailingButton: UIButton!\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        view.addLayoutGuide(focusGuide)\n        focusGuide.preferredFocusEnvironments = [trailingButton]\n\n        NSLayoutConstraint.activate([\n            focusGuide.leadingAnchor.constraint(equalTo: leadingButton.trailingAnchor),\n            focusGuide.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor),\n            focusGuide.topAnchor.constraint(equalTo: leadingButton.topAnchor),\n            focusGuide.bottomAnchor.constraint(equalTo: leadingButton.bottomAnchor)\n        ])\n    }\n}\n```\n\n`UIFocusGuide` is invisible and not a view. Use it to redirect focus without adding decorative UI.\n\n## Common Mistakes\n\n1. Mixing accessibility focus and keyboard or directional focus in the same mental model.\n2. Storing `@FocusState` in shared models instead of the owning view.\n3. Setting multiple competing default focus targets on one screen.\n4. Using `.focusable()` on decorative views.\n5. Forgetting focus restoration after sheets, popovers, or custom overlays.\n6. Reaching for `UIFocusGuide` before trying `focusSection()` on macOS or tvOS, or better layout grouping in SwiftUI.\n7. Using gesture handlers for primary actions on custom focusable controls instead of a semantic `Button` when possible.\n\n## Review Checklist\n\n- [ ] `@FocusState` is local to the view that owns the controls\n- [ ] Initial focus target is explicit when the screen needs one\n- [ ] Focus movement between fields or groups is deterministic\n- [ ] `focusedSceneValue` or related focused-value APIs are used when commands need current scene state\n- [ ] Custom controls opt into focus only when they are truly interactive\n- [ ] `focusSection()` is used for uneven directional layouts on macOS or tvOS before dropping to UIKit\n- [ ] Focus returns to a stable element after temporary presentations dismiss\n- [ ] `UIFocusGuide` geometry and preferred destinations match the intended route\n- [ ] Accessibility focus concerns are handled in `ios-accessibility`, not mixed into keyboard-directional focus logic\n\n## References\n\n- Detailed patterns: [references/focus-patterns.md](references/focus-patterns.md)\n- Multi-platform focus (tvOS, watchOS, visionOS, macOS): [references/multi-platform-focus.md](references/multi-platform-focus.md)\n- Focus debugging and anti-patterns: [references/focus-debugging.md](references/focus-debugging.md)","tags":["focus","engine","swift","ios","skills","dpearson2699","accessibility","agent-skills","ai-coding","apple","claude-code","codex-skills"],"capabilities":["skill","source-dpearson2699","skill-focus-engine","topic-accessibility","topic-agent-skills","topic-ai-coding","topic-apple","topic-claude-code","topic-codex-skills","topic-cursor-skills","topic-ios","topic-ios-development","topic-liquid-glass","topic-localization","topic-mapkit"],"categories":["swift-ios-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/dpearson2699/swift-ios-skills/focus-engine","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add dpearson2699/swift-ios-skills","source_repo":"https://github.com/dpearson2699/swift-ios-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 599 github stars · SKILL.md body (8,257 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T18:53:41.713Z","embedding":null,"createdAt":"2026-04-23T00:53:23.194Z","updatedAt":"2026-05-18T18:53:41.713Z","lastSeenAt":"2026-05-18T18:53:41.713Z","tsv":"'0.15':459 '1':703 '12':455 '13':499 '15':502 '2':717 '26':52 '3':728 '4':738 '5':744 '6':754 '7':771 'access':83,96,349,705,879,887 'accessibility-specif':82 'across':13,507,637 'act':406 'action':439,451,452,777 'activ':468,472 'ad':698 'anti':915 'anti-pattern':914 'api':825 'app':49 'appear':267 'appli':77 'automat':273 'awkward':641 'behavior':12,44,492 'better':766 'bind':366,371,382 'bodi':205,290,387,447,520,594 'bool':168,445,592 'broader':479 'button':294,300,450,475,525,527,529,533,535,538,597,786 'button-lik':474 'buttonstyl':464 'case':188,231,235,281 'chang':618 'checklist':148,151,790 'class':645 'clear':311,460 'color.accentcolor.opacity':458 'command':341,400,829 'common':142,145,701 'common-mistak':144 'compet':731 'concern':881 'content':98,413 'continu':539 'control':90,250,263,477,781,800,835 'cornerradius':454 'cover':57 'crown':38 'current':332,412,831 'custom':420,635,752,779,834 'dashboardviewcontrol':646 'debug':912 'decor':699,742 'default':104,107,251,312,550,732 'default-focus':106 'defaultfocus':21,254,306 'descend':511 'destin':313,874 'detail':897 'determinist':818 'digit':37 'direct':6,60,429,505,710,850,893 'dismiss':562,869 'disori':623 'download':530 'driven':491 'drop':857 'edit':486 'element':865 'email':189,194,210,212,216,227,232 'empti':638 'engin':3,42 'enum':177,185,278 'equal':215,223,298,304 'equalto':674,677,680,683 'even':353 'explicit':805 'expos':328 'extens':367 'fals':587 'favorit':528 'featur':534 'field':186,203,814 'fill':456 'filter':598 'filtereditor':606 'filtersview':581 'final':644 'focus':2,11,22,24,26,30,39,41,43,59,61,64,66,70,85,105,108,109,115,120,123,125,128,130,133,136,140,162,213,221,240,249,252,260,270,296,302,317,319,325,333,338,355,409,414,417,430,466,469,490,493,510,543,559,570,601,614,625,696,706,711,733,740,746,780,802,811,823,838,860,880,894,904,911 'focus-driven':489 'focus-engin':1 'focus-restor':132 'focus-sect':127 'focusable-interact':122 'focused-valu':822 'focused-values-and-scene-valu':114 'focusedfield':202,214,222,226,230,233 'focusedscenevalu':392,819 'focusedtarget':287,297,303,307 'focusedvalu':368 'focusedvaluekey':363 'focusguid':650,669 'focusguide.bottomanchor.constraint':682 'focusguide.leadinganchor.constraint':673 'focusguide.preferredfocusenvironments':670 'focusguide.topanchor.constraint':679 'focusguide.trailinganchor.constraint':676 'focussect':72,496,531,541,760,845 'focusst':20,100,103,155,157,199,284,441,588,719,791 'forget':745 'form':208 'func':665 'genuin':484 'geometr':29 'geometri':642,871 'gestur':773 'get':372 'group':508,558,768,816 'guid':71,137,141,504,626 'guidanc':73 'handl':883 'handler':774 'hashabl':176,187,280 'hstack':523 'iboutlet':652,658 'implement':4 'initi':259,801 'insid':164 'instead':723,782 'intend':557,877 'intent':615 'interact':25,121,124,415,418,467,480,844 'invis':687 'io':51,95,886 'ios-access':94,885 'ipado':53 'isfilterbuttonfocus':591,602,611 'isfocus':444,457,470 'ispres':604 'keep':239,348 'keyboard':5,58,427,708,892 'keyboard-direct':891 'layout':514,633,767,851 'leadingbutton':656 'leadingbutton.bottomanchor':684 'leadingbutton.topanchor':681 'leadingbutton.trailinganchor':675 'leav':621 'left/right':551 'let':435,438,649 'level':10 'librari':282,295,299,308 'like':476 'live':91 'local':242,793 'logic':576,895 'loginview':183 'maco':54,79,498,546,762,853,908 'mainactor':609 'manag':19 'match':875 'mental':715 'menus':399 'mistak':143,146,702 'mix':704,889 'model':31,716,722 'move':356 'movement':506,554,812 'multi':902 'multi-platform':901 'multipl':179,488,730 'navig':35 'need':404,485,634,809,830 'newvalu':378 'next':577 'nil':237 'nslayoutconstraint.activate':672 'onappear':225 'ondisappear':607 'one':310,736,810 'onsubmit':228 'opt':836 'option':175 'otherwis':620 'overlay':461,568,753 'overrid':664 'own':247,726,798 'particip':425 'password':190,198,218,220,224,234,236 'pattern':397,898,916 'per':314 'pick':537 'placement':163 'plain':465 'platform':903 'popov':565,750 'possibl':788 'prefer':258,309,873 'present':617,868 'primari':776 'privat':192,196,200,285,442,584,589,648,653,659 'reach':755 'read':159 'reassign':272 'recent':526 'recip':384,385,394 'recipe.title':391 'recipedetailview':380 'redirect':695 'refer':152,153,896 'references/focus-debugging.md':917,918 'references/focus-patterns.md':899,900 'references/multi-platform-focus.md':909,910 'region':261,318 'relat':821 'remot':34 'reserv':478 'restor':67,131,134,560,613,747 'return':569,861 'review':147,150,789 'review-checklist':149 'roundedrectangl':453 'rout':636,878 'scene':9,63,112,118,166,322,337,344,359,410,832 'scene-focus':62,336 'scene-level':8 'scene-wid':343 'screen':315,737,808 'section':27,126,129,494,544 'securefield':217 'selectablecard':433 'selectedrecip':370,393 'selectedrecipekey':362 'selectedrecipekey.self':374,377 'self':373,376 'semant':785 'set':256,283,301,305,375,729 'share':721 'sheet':564,603,749 'showsheet':586,599,605 'sidebarview':276 'singl':171 'siri':33 'skill':76,97 'skill-focus-engine' 'skip':555 'source-dpearson2699' 'space':639 'specif':84 'stabl':573,864 'state':191,195,241,329,583,833 'store':718 'string':437 'struct':182,275,361,379,432,516,580 'submit':238 'super.viewdidload':667 'swift':181,274,360,431,515,579,643 'swiftui':14,46,99,102,154,421,770 'swiftui-focusst':101 'switch':89,229 'target':50,172,180,279,288,578,734,803 'task':608 'temporari':867 'text':211,219,390,462 'textfield':209 'titl':436,463 'toolbar':402 'top':536 'topic-accessibility' 'topic-agent-skills' 'topic-ai-coding' 'topic-apple' 'topic-claude-code' 'topic-codex-skills' 'topic-cursor-skills' 'topic-ios' 'topic-ios-development' 'topic-liquid-glass' 'topic-localization' 'topic-mapkit' 'trailingbutton':662,671 'trailingbutton.leadinganchor':678 'transient':567 'tri':759 'trigger':574 'true':600,612 'truli':843 'tvlibraryview':517 'tvos':28,56,81,501,548,632,764,855,905 'typealia':364 'ui':346,700 'uibutton':657,663 'uifocusguid':628,651,685,757,870 'uikit':16,48,69,135,139,624,630,859 'uikit-focus-guid':138 'uiviewcontrol':647 'uneven':513,849 'up/down':553 'use':17,156,167,253,324,335,395,416,471,495,542,627,692,739,772,827,847 'user':622 'valu':23,65,110,113,116,119,320,323,326,339,352,365,824 'var':193,197,201,204,286,289,369,383,386,443,446,519,585,590,593,655,661 'view':184,207,245,266,277,292,334,381,389,422,434,449,482,518,522,582,596,691,727,743,796 'view.addlayoutguide':668 'viewdidload':666 'visi':40 'visiono':907 'voiceov':87 'void':440 'vstack':293,524,532 'watch':540 'watcho':36,906 'weak':654,660 'whenev':616 'wide':345 'within':357 'without':697 'would':619 'write':161","prices":[{"id":"3fef90e2-fa77-4ee3-bd68-1877cf85b859","listingId":"2562bcbc-6c2d-44f8-bb3d-fb0436cb29ba","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"dpearson2699","category":"swift-ios-skills","install_from":"skills.sh"},"createdAt":"2026-04-23T00:53:23.194Z"}],"sources":[{"listingId":"2562bcbc-6c2d-44f8-bb3d-fb0436cb29ba","source":"github","sourceId":"dpearson2699/swift-ios-skills/focus-engine","sourceUrl":"https://github.com/dpearson2699/swift-ios-skills/tree/main/skills/focus-engine","isPrimary":false,"firstSeenAt":"2026-04-23T00:53:23.194Z","lastSeenAt":"2026-05-18T18:53:41.713Z"},{"listingId":"2562bcbc-6c2d-44f8-bb3d-fb0436cb29ba","source":"skills_sh","sourceId":"dpearson2699/swift-ios-skills/focus-engine","sourceUrl":"https://skills.sh/dpearson2699/swift-ios-skills/focus-engine","isPrimary":true,"firstSeenAt":"2026-05-07T20:42:00.506Z","lastSeenAt":"2026-05-07T22:41:20.636Z"}],"details":{"listingId":"2562bcbc-6c2d-44f8-bb3d-fb0436cb29ba","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"dpearson2699","slug":"focus-engine","github":{"repo":"dpearson2699/swift-ios-skills","stars":599,"topics":["accessibility","agent-skills","ai-coding","apple","claude-code","codex-skills","cursor-skills","ios","ios-development","liquid-glass","localization","mapkit","networking","storekit","swift","swift-concurrency","swiftdata","swiftui","widgetkit","xcode"],"license":"other","html_url":"https://github.com/dpearson2699/swift-ios-skills","pushed_at":"2026-04-26T21:04:17Z","description":"Agent Skills for iOS 26+, Swift 6.3, SwiftUI, and modern Apple frameworks","skill_md_sha":"c69594f69724911c8a8ee93fc8b712f6b71301ac","skill_md_path":"skills/focus-engine/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/dpearson2699/swift-ios-skills/tree/main/skills/focus-engine"},"layout":"multi","source":"github","category":"swift-ios-skills","frontmatter":{"name":"focus-engine","description":"Implements keyboard, directional, and scene-level focus behavior across SwiftUI and UIKit. Use when managing @FocusState, defaultFocus, focused values, focusable interactions, focus sections, tvOS geometric focus model and Siri Remote navigation, watchOS Digital Crown focus, visionOS gaze/hover and RealityKit InputTargetComponent, macOS key view loop and Full Keyboard Access, focus restoration after presentation changes, custom focus routing with UIFocusGuide, or debugging focus with UIFocusDebugger."},"skills_sh_url":"https://skills.sh/dpearson2699/swift-ios-skills/focus-engine"},"updatedAt":"2026-05-18T18:53:41.713Z"}}