focus-engine

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Focus Engine

焦点引擎

Focus 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.
面向iOS 26+、iPadOS、macOS和tvOS的SwiftUI与UIKit应用的焦点行为。涵盖键盘焦点、方向焦点、场景焦点值、焦点恢复以及UIKit焦点引导。本技能中的
focusSection()
指南适用于macOS和tvOS。VoiceOver与Switch Control的无障碍专属焦点相关内容请查看
ios-accessibility
技能。

Contents

目录

SwiftUI FocusState

SwiftUI FocusState

Use
@FocusState
to read and write focus placement inside a scene. Use
Bool
for a single target or an optional
Hashable
enum for multiple targets.
swift
struct LoginView: View {
    enum Field: Hashable { case email, password }

    @State private var email = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        Form {
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
        }
        .onAppear { focusedField = .email }
        .onSubmit {
            switch focusedField {
            case .email: focusedField = .password
            case .password, nil: submit()
            }
        }
    }
}
Keep focus state local to the view that owns the focusable controls.
使用
@FocusState
读取和写入场景内的焦点位置。单个目标使用
Bool
类型,多个目标使用可选的
Hashable
枚举类型。
swift
struct LoginView: View {
    enum Field: Hashable { case email, password }

    @State private var email = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?

    var body: some View {
        Form {
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
        }
        .onAppear { focusedField = .email }
        .onSubmit {
            switch focusedField {
            case .email: focusedField = .password
            case .password, nil: submit()
            }
        }
    }
}
将焦点状态限定在拥有可焦点控件的视图内部。

Default Focus

默认焦点

Use
.defaultFocus
to set the preferred initial focus region or control when a view appears or when focus is reassigned automatically.
swift
struct SidebarView: View {
    enum Target: Hashable { case library, settings }
    @FocusState private var focusedTarget: Target?

    var body: some View {
        VStack {
            Button("Library") { }
                .focused($focusedTarget, equals: .library)

            Button("Settings") { }
                .focused($focusedTarget, equals: .settings)
        }
        .defaultFocus($focusedTarget, .library)
    }
}
Prefer one clear default destination per screen or focus region.
使用
.defaultFocus
在视图出现或焦点自动重新分配时,设置首选的初始焦点区域或控件。
swift
struct SidebarView: View {
    enum Target: Hashable { case library, settings }
    @FocusState private var focusedTarget: Target?

    var body: some View {
        VStack {
            Button("Library") { }
                .focused($focusedTarget, equals: .library)

            Button("Settings") { }
                .focused($focusedTarget, equals: .settings)
        }
        .defaultFocus($focusedTarget, .library)
    }
}
每个屏幕或焦点区域最好设置一个清晰的默认焦点目标。

Focused Values and Scene Values

焦点值与场景值

Use 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.
swift
struct SelectedRecipeKey: FocusedValueKey {
    typealias Value = Binding<Recipe>
}

extension FocusedValues {
    var selectedRecipe: Binding<Recipe>? {
        get { self[SelectedRecipeKey.self] }
        set { self[SelectedRecipeKey.self] = newValue }
    }
}

struct RecipeDetailView: View {
    @Binding var recipe: Recipe

    var body: some View {
        Text(recipe.title)
            .focusedSceneValue(\.selectedRecipe, $recipe)
    }
}
Use this pattern for menus, commands, and toolbars that need to act on the focused scene's current content.
使用焦点值暴露当前焦点视图的状态。当命令或场景级UI需要在焦点在场景内移动后仍能访问该值时,使用场景焦点值。
swift
struct SelectedRecipeKey: FocusedValueKey {
    typealias Value = Binding<Recipe>
}

extension FocusedValues {
    var selectedRecipe: Binding<Recipe>? {
        get { self[SelectedRecipeKey.self] }
        set { self[SelectedRecipeKey.self] = newValue }
    }
}

struct RecipeDetailView: View {
    @Binding var recipe: Recipe

    var body: some View {
        Text(recipe.title)
            .focusedSceneValue(\.selectedRecipe, $recipe)
    }
}
此模式适用于需要对焦点场景的当前内容执行操作的菜单、命令和工具栏。

Focusable Interactions

可焦点交互

Use
.focusable(_:interactions:)
on custom SwiftUI views that should participate in keyboard or directional focus.
swift
struct SelectableCard: View {
    let title: String
    let action: () -> Void
    @FocusState private var isFocused: Bool

    var body: some View {
        Button(action: action) {
            RoundedRectangle(cornerRadius: 12)
                .fill(isFocused ? Color.accentColor.opacity(0.15) : .clear)
                .overlay { Text(title) }
        }
        .buttonStyle(.plain)
        .focusable(interactions: .activate)
        .focused($isFocused)
    }
}
Use
.activate
for button-like controls. Reserve broader interactions for views that genuinely need editing or multiple focus-driven behaviors.
在需要参与键盘或方向焦点的自定义SwiftUI视图上使用
.focusable(_:interactions:)
swift
struct SelectableCard: View {
    let title: String
    let action: () -> Void
    @FocusState private var isFocused: Bool

    var body: some View {
        Button(action: action) {
            RoundedRectangle(cornerRadius: 12)
                .fill(isFocused ? Color.accentColor.opacity(0.15) : .clear)
                .overlay { Text(title) }
        }
        .buttonStyle(.plain)
        .focusable(interactions: .activate)
        .focused($isFocused)
    }
}
类按钮控件使用
.activate
。只有真正需要编辑或多种焦点驱动行为的视图才使用更广泛的交互类型。

Focus Sections

焦点区域

Use
focusSection()
on macOS 13+ and tvOS 15+ to guide directional movement across groups of focusable descendants in uneven layouts.
swift
struct TVLibraryView: View {
    var body: some View {
        HStack {
            VStack {
                Button("Recent") { }
                Button("Favorites") { }
                Button("Downloaded") { }
            }
            .focusSection()

            VStack {
                Button("Featured") { }
                Button("Top Picks") { }
                Button("Continue Watching") { }
            }
            .focusSection()
        }
    }
}
Use focus sections on macOS and tvOS when default left/right or up/down movement skips the intended group.
在macOS 13+和tvOS 15+上使用
focusSection()
,引导焦点在布局不均匀的可焦点子视图组之间进行方向移动。
swift
struct TVLibraryView: View {
    var body: some View {
        HStack {
            VStack {
                Button("Recent") { }
                Button("Favorites") { }
                Button("Downloaded") { }
            }
            .focusSection()

            VStack {
                Button("Featured") { }
                Button("Top Picks") { }
                Button("Continue Watching") { }
            }
            .focusSection()
        }
    }
}
当默认的左右或上下焦点移动跳过了预期的视图组时,在macOS和tvOS上使用焦点区域。

Focus Restoration

焦点恢复

After dismissing a sheet, popover, or transient overlay, return focus to a stable trigger or logical next target.
swift
struct FiltersView: View {
    @State private var showSheet = false
    @FocusState private var isFilterButtonFocused: Bool

    var body: some View {
        Button("Filters") { showSheet = true }
            .focused($isFilterButtonFocused)
            .sheet(isPresented: $showSheet) {
                FilterEditor()
                    .onDisappear {
                        Task { @MainActor in
                            isFilterButtonFocused = true
                        }
                    }
            }
    }
}
Restore focus intentionally whenever presentation changes would otherwise leave users disoriented.
在关闭弹窗、弹出层或临时覆盖视图后,将焦点返回到稳定的触发控件或逻辑上的下一个目标。
swift
struct FiltersView: View {
    @State private var showSheet = false
    @FocusState private var isFilterButtonFocused: Bool

    var body: some View {
        Button("Filters") { showSheet = true }
            .focused($isFilterButtonFocused)
            .sheet(isPresented: $showSheet) {
                FilterEditor()
                    .onDisappear {
                        Task { @MainActor in
                            isFilterButtonFocused = true
                        }
                    }
            }
    }
}
每当视图展示变更可能使用户迷失方向时,都要主动恢复焦点。

UIKit Focus Guides

UIKit焦点引导

Use
UIFocusGuide
when UIKit or tvOS layouts need custom routing across empty space or awkward geometry.
swift
final class DashboardViewController: UIViewController {
    private let focusGuide = UIFocusGuide()
    @IBOutlet private weak var leadingButton: UIButton!
    @IBOutlet private weak var trailingButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addLayoutGuide(focusGuide)
        focusGuide.preferredFocusEnvironments = [trailingButton]

        NSLayoutConstraint.activate([
            focusGuide.leadingAnchor.constraint(equalTo: leadingButton.trailingAnchor),
            focusGuide.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor),
            focusGuide.topAnchor.constraint(equalTo: leadingButton.topAnchor),
            focusGuide.bottomAnchor.constraint(equalTo: leadingButton.bottomAnchor)
        ])
    }
}
UIFocusGuide
is invisible and not a view. Use it to redirect focus without adding decorative UI.
当UIKit或tvOS布局需要跨空白区域或不规则几何结构进行自定义焦点路由时,使用
UIFocusGuide
swift
final class DashboardViewController: UIViewController {
    private let focusGuide = UIFocusGuide()
    @IBOutlet private weak var leadingButton: UIButton!
    @IBOutlet private weak var trailingButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addLayoutGuide(focusGuide)
        focusGuide.preferredFocusEnvironments = [trailingButton]

        NSLayoutConstraint.activate([
            focusGuide.leadingAnchor.constraint(equalTo: leadingButton.trailingAnchor),
            focusGuide.trailingAnchor.constraint(equalTo: trailingButton.leadingAnchor),
            focusGuide.topAnchor.constraint(equalTo: leadingButton.topAnchor),
            focusGuide.bottomAnchor.constraint(equalTo: leadingButton.bottomAnchor)
        ])
    }
}
UIFocusGuide
是不可见的,并非视图。使用它可以在不添加装饰性UI的情况下重定向焦点。

Common Mistakes

常见错误

  1. Mixing accessibility focus and keyboard or directional focus in the same mental model.
  2. Storing
    @FocusState
    in shared models instead of the owning view.
  3. Setting multiple competing default focus targets on one screen.
  4. Using
    .focusable()
    on decorative views.
  5. Forgetting focus restoration after sheets, popovers, or custom overlays.
  6. Reaching for
    UIFocusGuide
    before trying
    focusSection()
    on macOS or tvOS, or better layout grouping in SwiftUI.
  7. Using gesture handlers for primary actions on custom focusable controls instead of a semantic
    Button
    when possible.
  1. 将无障碍焦点与键盘或方向焦点混为一谈。
  2. 在共享模型中存储
    @FocusState
    ,而非在拥有控件的视图中存储。
  3. 在一个屏幕上设置多个相互冲突的默认焦点目标。
  4. 在装饰性视图上使用
    .focusable()
  5. 关闭弹窗、弹出层或自定义覆盖视图后忘记恢复焦点。
  6. 在macOS或tvOS上未尝试
    focusSection()
    ,也未优化SwiftUI布局分组就直接使用
    UIFocusGuide
  7. 在自定义可焦点控件上使用手势处理器处理主要操作,而非尽可能使用语义化的
    Button

Review Checklist

检查清单

  • @FocusState
    is local to the view that owns the controls
  • Initial focus target is explicit when the screen needs one
  • Focus movement between fields or groups is deterministic
  • focusedSceneValue
    or related focused-value APIs are used when commands need current scene state
  • Custom controls opt into focus only when they are truly interactive
  • focusSection()
    is used for uneven directional layouts on macOS or tvOS before dropping to UIKit
  • Focus returns to a stable element after temporary presentations dismiss
  • UIFocusGuide
    geometry and preferred destinations match the intended route
  • Accessibility focus concerns are handled in
    ios-accessibility
    , not mixed into keyboard-directional focus logic
  • @FocusState
    限定在拥有控件的视图内部
  • 当屏幕需要初始焦点时,明确设置初始焦点目标
  • 字段或视图组之间的焦点移动具有确定性
  • 当命令需要当前场景状态时,使用
    focusedSceneValue
    或相关的焦点值API
  • 自定义控件仅在真正具有交互性时才启用焦点
  • 在macOS或tvOS上,针对不均匀的方向布局,先使用
    focusSection()
    再降级到UIKit实现
  • 临时视图关闭后,焦点返回到稳定的元素
  • UIFocusGuide
    的几何结构和首选目标与预期的路由匹配
  • 无障碍焦点相关问题在
    ios-accessibility
    中处理,未与键盘/方向焦点逻辑混淆

References

参考资料

  • Detailed patterns: references/focus-patterns.md
  • Multi-platform focus (tvOS, watchOS, visionOS, macOS): references/multi-platform-focus.md
  • Focus debugging and anti-patterns: references/focus-debugging.md
  • 详细模式:references/focus-patterns.md
  • 多平台焦点(tvOS、watchOS、visionOS、macOS):references/multi-platform-focus.md
  • 焦点调试与反模式:references/focus-debugging.md