关于 focus-engine
focus-engine 是一项开发技能,指导在 SwiftUI 和 UIKit 应用中实现键盘与方向焦点。内容涵盖焦点状态、场景值、焦点恢复和平台相关路由,并区分辅助功能焦点与 visionOS 凝视输入。
主要功能
- 01使用 SwiftUI FocusState 管理焦点
- 02设置默认焦点目标
- 03提供焦点值和场景值
- 04用焦点分区引导方向移动
- 05恢复焦点并用 UIKit 焦点指南路由
适用场景
- 01控制表单字段间的键盘焦点
- 02导航 macOS 或 tvOS 的不规则布局
- 03让命令关联当前场景焦点内容
- 04关闭覆盖层后恢复焦点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill focus-engine安装后在 Claude Code 中输入/focus-engine确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/focus-debugging.mdfocus-patterns.mdmulti-platform-focus.md详细介绍
焦点引擎
适用于面向 iOS 26+、iPadOS、macOS、tvOS 和 visionOS 连接输入路径的 SwiftUI 与 UIKit 应用的焦点行为。涵盖键盘焦点、方向焦点、场景焦点值、焦点恢复和 UIKit 焦点引导。此技能中关于 focusSection() 的指南适用于 macOS 和 tvOS。visionOS 中由注视驱动的悬停是一种输入方式,而非焦点。VoiceOver 和 Switch Control 的无障碍专用焦点内容请参阅 ios-accessibility 技能。
当请求同时涉及焦点、无障碍或空间输入时,请明确界定范围:
- 键盘、遥控器、游戏控制器和场景焦点行为请使用此技能。
- 对于 visionOS,请将注视、直接触摸和指针定位描述为悬停/输入方式,而非焦点。
- 对于 VoiceOver、Switch Control、Voice Control 或无障碍元素排序,请简要转交给
ios-accessibility。
目录
SwiftUI FocusState
使用 @FocusState 在场景中读取和设置焦点位置。单个目标使用 Bool,多个目标使用可选的 Hashable 枚举。
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()
}
}
}
}
将焦点状态保留在拥有可聚焦控件的视图中。
默认焦点
使用 .defaultFocus 设置视图出现时或自动重新分配焦点时首选的初始焦点区域或控件。
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)
}
}
每个屏幕或焦点区域最好只设置一个明确的默认目标。
焦点值和场景值
使用焦点值公开当前获得焦点的视图中的状态。如果命令或场景级 UI 在焦点于场景内移动后仍需访问该值,请使用场景焦点值。
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)
}
}
对于需要操作当前焦点所在场景内容的菜单、命令和工具栏,请使用此模式。
可聚焦交互
对于需要参与键盘或方向焦点的自定义 SwiftUI 视图,请使用 .focusable(_:interactions:)。
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)
}
}
优先使用语义化的 Button、Toggle、TextField 和其他系统控件,而不是让任意手势驱动的视图可聚焦。只有当语义化控件无法表达 UI 时,才对类似按钮的自定义控件使用 .focusable(interactions: .activate)。仅当视图确实需要编辑或多种焦点驱动行为时,才使用范围更广的交互。
焦点区域
在 macOS 13+ 和 tvOS 15+ 上,当不规则布局中默认的左右或上下移动会跳过预期分组时,使用 focusSection() 引导焦点在可聚焦后代视图分组之间移动。
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 上使用焦点区域。
焦点恢复
关闭工作表、弹出框或临时覆盖层后,将焦点移回稳定的触发控件或逻辑上的下一个目标。
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 焦点引导
当 UIKit 或 tvOS 布局需要跨越空白区域或不规则几何区域自定义焦点路径时,请使用 UIFocusGuide。
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。
常见错误
- 在同一套思路中混淆辅助功能焦点与键盘或方向焦点。
- 将
@FocusState存储在共享模型中,而不是所属视图中。 - 在同一屏幕上设置多个互相冲突的默认焦点目标。
- 对装饰性视图使用
.focusable()。 - 忘记在工作表、弹出窗口或自定义覆盖层关闭后恢复焦点。
- 在 macOS 或 tvOS 上未先尝试使用
focusSection(),或在 SwiftUI 中更好地组织布局分组,就直接使用UIFocusGuide。 - 对自定义可聚焦控件使用手势处理器来实现主要操作,而不是尽可能使用语义明确的
Button。 - 将 visionOS 的视线悬停视为焦点;仅对键盘和游戏控制器等连接的输入设备使用焦点引导。
审查清单
-
@FocusState位于拥有这些控件的视图中 - 如果屏幕需要初始焦点目标,则明确指定
- 字段或分组之间的焦点移动是确定的
- 命令需要当前场景状态时,使用
focusedSceneValue或相关的 focused-value API - 仅当自定义控件确实可交互时,才启用焦点
- 在 macOS 或 tvOS 上,遇到不规则方向布局时,先使用
focusSection(),再考虑转而使用 UIKit - 临时界面关闭后,焦点会返回到稳定的元素
-
UIFocusGuide的几何布局和首选目标与预期路径一致 - visionOS 引导会区分连接设备焦点与视线驱动的悬停或 RealityKit 输入目标
- 辅助功能焦点问题在
ios-accessibility中处理,不与键盘方向焦点逻辑混为一谈
参考资料
- 详细模式:references/focus-patterns.md
- 多平台焦点(tvOS、watchOS、visionOS、macOS):references/multi-platform-focus.md
- 焦点调试与反模式:references/focus-debugging.md