Highlight
iOS 的 Full Keyboard Access 让用户只用键盘操作 iPhone 和 iPad,开发者可以通过自定义操作、键盘快捷键、交互性标记、输入标签和无障碍路径让应用适配键盘用户。
核心内容
有些用户不能稳定地触摸屏幕。手指活动范围有限、手部震颤、精细操作困难,都会让点按、长按、拖拽变成负担。
iOS 已经提供 AssistiveTouch(辅助触控)、Switch Control(切换控制)和 Voice Control(语音控制)。Full Keyboard Access(全键盘访问)补上了另一种输入方式:用户可以只用外接键盘完成系统导航、元素选择、操作菜单、查找和手势模式。
这件事对 iPad 尤其重要。用户可以用方向键移动,用空格激活,用 Tab 进入不同区域,用 Tab-Z 打开操作菜单,用 Tab-F 查找界面元素。键盘从辅助输入变成完整输入路径。
开发者遇到的问题会出现在细节里。按钮能被 Tab 选中,但按空格没有反应;卡片支持长按置顶,但键盘用户找不到这个动作;设置按钮只有齿轮图标,用户不知道该搜索 “settings” 还是 “prefs”。
Apple 在这场 session 里用一个叫 Shape Shuffle 的小游戏演示修复过程。核心做法是补充已有控件的无障碍信息:这个元素有什么动作、什么时候能响应、用户还可能用哪些词找到它、焦点轮廓应该是什么形状。
详细内容
给常用动作添加 Accessibility Custom Action
(07:06)Shape Shuffle 里的卡片支持两个动作:加入棋盘和加 pin。触摸用户可以双击或长按,键盘用户需要在 Tab-Z 操作菜单里看到同样的动作。
UIKit 的入口是 UIAccessibilityCustomAction。把动作挂到 accessibilityCustomActions 后,VoiceOver、Switch Control 和 Full Keyboard Access 都能发现它。
// Accessibility custom actions
let addAction = UIAccessibilityCustomAction(
name: gameLocString("add"), image: UIImage(systemName: "plus.square")) { _ in
self.addCard()
return true
}
let pinAction = UIAccessibilityCustomAction(
name: gameLocString("pin"), image: UIImage(systemName: "pin.fill")) { _ in
self.pinCard()
return true
}
cardView.accessibilityCustomActions = [addAction, pinAction]
关键点:
UIAccessibilityCustomAction定义一个辅助技术可调用的动作。name使用本地化字符串,操作菜单会显示这段文字。image对 Full Keyboard Access 不显示,但会在 Switch Control 中使用。- 闭包里调用原有业务方法
addCard()和pinCard(),成功后返回true。 cardView.accessibilityCustomActions把两个动作绑定到当前卡片。
给键盘用户提供快捷键
(08:39)自定义操作适合辅助技术用户。键盘快捷键覆盖面更广:开启 Full Keyboard Access 的用户能用,普通外接键盘用户也能用。
iOS 和 iPadOS 15 把 Mac Catalyst 13 的菜单构建 API 带到 iOS。开发者可以在 AppDelegate 里覆盖 buildMenu(with:),创建 UIKeyCommand,再插入 UIMenuBuilder。
// Keyboard shortcuts
// In AppDelegate.swift
override func buildMenu(with builder: UIMenuBuilder) {
super.buildMenu(with: builder)
guard builder.system == .main else {
return
}
let pinCommand = UIKeyCommand(
title: gameLocString("pin"),
image: UIImage(systemName: "pin.fill"),
action: #selector(GameViewController.pinFocusedCard),
input: "P",
discoverabilityTitle: gameLocString("pin.card")
)
let addCommand = UIKeyCommand(
title: gameLocString("add"),
image: UIImage(systemName: "plus.square"),
action: #selector(GameViewController.addFocusedCard),
input: "A",
discoverabilityTitle: gameLocString("add.card")
)
let identifier = UIMenu.Identifier("gameplay_menu")
let menu = UIMenu(
title: gameLocString("gameplay"),
image: UIImage(systemName: "rectangle.grid.3x2"),
identifier: identifier,
children: [addCommand, pinCommand]
)
builder.insertSibling(menu, afterMenu: .view)
}
关键点:
buildMenu(with:)是向系统菜单加入命令的位置。guard builder.system == .main限制这段逻辑只作用于主菜单。UIKeyCommand把标题、图标、selector 和按键绑定在一起。input: "P"和input: "A"分别定义 pin 和 add 的快捷键。discoverabilityTitle会出现在按住 Command 后显示的快捷键面板里。UIMenu把两个命令归入 “gameplay” 菜单。builder.insertSibling(menu, afterMenu: .view)把菜单插到 View 菜单之后。
只在有焦点卡片时启用命令
(09:22)快捷键不能永远可用。Shape Shuffle 只有在选中卡片后,add 和 pin 才有意义。
UIKit 用 responder chain(响应者链)决定 action 是否可执行。覆盖 canPerformAction(_:withSender:),可以让快捷键面板只在正确状态下显示命令。
// Keyboard shortcuts
// In GameViewController.swift
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(addFocusedCard) || action == #selector(pinFocusedCard) {
return self.focusedCard != .none
}
return super.canPerformAction(action, withSender: sender)
}
关键点:
canPerformAction决定某个 selector 当前是否可执行。#selector(addFocusedCard)对应上一个示例里的 add 快捷键。#selector(pinFocusedCard)对应 pin 快捷键。self.focusedCard != .none确保用户已经选中一张卡片。- 其他 action 交给
super,避免影响系统菜单和其他控制器。
让焦点跳过不能交互的元素
(10:35)开发者为 VoiceOver 做过适配后,经常会把很多视图标成无障碍元素。Shape Shuffle 的三角形、颜色和数字需要被 VoiceOver 读出来,所以代码会设置 isAccessibilityElement 和 accessibilityLabel。
itemView.isAccessibilityElement = true
itemView.accessibilityLabel = gameLocString(for: item)
关键点:
isAccessibilityElement = true让itemView进入无障碍树。accessibilityLabel提供 VoiceOver 读取的文本。- Full Keyboard Access 的光标也可能访问这些无障碍元素。
- 如果元素只能被读取,不能被操作,键盘用户会遇到“选中了但按空格没反应”的假焦点。
(11:01)解决办法是设置 accessibilityRespondsToUserInteraction。当它为 false 时,元素仍然可被 VoiceOver 读取,但 Full Keyboard Access 和 Switch Control 这类运动辅助技术会跳过它。
itemView.accessibilityRespondsToUserInteraction = false
关键点:
- 这个属性只在
isAccessibilityElement已经为true时有意义。 - 系统默认会尝试判断一个无障碍元素是否可交互。
- 当一个元素服务于 VoiceOver 读取,但不应该接受键盘或切换控制焦点时,手动设为
false。 - 不要为了 Full Keyboard Access 去覆盖
canBecomeFocused,因为它会影响整个焦点引擎,也会影响未开启 Full Keyboard Access 的 Tab 导航。
给查找功能提供更多输入标签
(13:41)Full Keyboard Access 有 Find(查找)功能。用户按 Tab-F 后,可以输入文字快速跳到界面元素。
问题出在图标按钮上。设置按钮可能叫 “Settings”,用户也可能输入 “prefs”、“preferences” 或 “gear”。accessibilityUserInputLabels 允许开发者给同一个控件提供多组可输入名称。
self.accessibilityUserInputLabels = [
gameLocString("settings"),
gameLocString("prefs"),
gameLocString("preferences"),
gameLocString("gear")
]
关键点:
accessibilityUserInputLabels是字符串数组。- 每个字符串都应该本地化,和界面语言保持一致。
- Full Keyboard Access 的 Find 会使用这些字符串匹配元素。
- Voice Control 也会使用这些名称,用户可以说出任意一个名称来触发控件。
- 这不会替换 VoiceOver 的
accessibilityLabel。
用 Accessibility Path 改进焦点轮廓
(14:52)Full Keyboard Access 的光标默认用矩形包围控件。Shape Shuffle 的关卡按钮是圆形、三角形和方形,矩形光标不贴合视觉形状。
accessibilityPath 可以告诉系统元素的真实形状。这个 API 同样会被 VoiceOver 使用。
// Accessibility path
let rect = circleLevelButton.convert(levelButton.bounds, to: nil)
circleLevelButton.accessibilityPath = UIBezierPath(ovalIn: rect)
关键点:
convert(_:to:)把按钮 bounds 转成屏幕坐标。accessibilityPath需要使用屏幕坐标。UIBezierPath(ovalIn:)创建圆形或椭圆形路径。- 设置后,Full Keyboard Access 的光标会贴合圆形按钮。
如果按钮在 UIScrollView 里,滚动会改变屏幕坐标。Apple 建议覆盖 accessibilityPath 或 accessibilityFrame,每次读取时重新计算。
// If your button is in a scroll view, it’s generally better to
// override accessibilityPath and/or accessibilityFrame
extension CircleButton {
open override var accessibilityPath: UIBezierPath? {
get {
let rect = self.convert(self.bounds, to: nil)
return UIBezierPath(ovalIn: rect)
}
set {
// no-op
}
}
}
关键点:
- 扩展
CircleButton,覆盖继承自UIView的accessibilityPath。 - getter 里每次用当前 bounds 重新计算屏幕坐标。
- 返回
UIBezierPath(ovalIn:),让系统得到最新的圆形路径。 - setter 留空,防止外部写入破坏动态计算结果。
核心启发
-
做什么:给卡片、邮件、任务、照片等列表项增加键盘操作菜单。 为什么值得做:
UIAccessibilityCustomAction会同时服务 Full Keyboard Access、VoiceOver 和 Switch Control,用户不用触屏也能执行收藏、归档、删除。 怎么开始:为每个列表项创建UIAccessibilityCustomAction,把业务方法放进 action 闭包,再赋值给accessibilityCustomActions。 -
做什么:为高频操作设计 iPad 键盘快捷键。 为什么值得做:
UIKeyCommand让开启 Full Keyboard Access 的用户和普通键盘用户使用同一套快捷操作。 怎么开始:在buildMenu(with:)里创建UIKeyCommand,给每个命令补上discoverabilityTitle,用canPerformAction控制可用状态。 -
做什么:清理“假焦点”。 为什么值得做:键盘光标落到不能交互的元素上,会让用户误以为应用卡住。 怎么开始:打开 Full Keyboard Access,用 Tab 遍历界面;发现只用于朗读的元素后,保留
isAccessibilityElement,把accessibilityRespondsToUserInteraction设为false。 -
做什么:给图标按钮增加多组搜索词。 为什么值得做:用户用 Find 或 Voice Control 时,不一定知道开发者给控件取的名字。 怎么开始:为设置、分享、筛选、收藏这类图标按钮设置
accessibilityUserInputLabels,写入常见同义词和口语名称。 -
做什么:让自定义形状控件的焦点轮廓匹配视觉形状。 为什么值得做:
accessibilityPath能让 Full Keyboard Access 和 VoiceOver 使用同一条真实轮廓,减少焦点位置和视觉界面的错位。 怎么开始:用UIBezierPath描述控件形状;静态控件直接设置accessibilityPath,滚动区域中的控件覆盖 getter 动态计算。
关联 Session
- SwiftUI Accessibility: Beyond the basics — 学习 SwiftUI 中的 Accessibility Preview、无障碍表示、子元素和焦点管理。
- Tailor the VoiceOver experience in your data-rich apps — 用自定义内容 API 改进复杂数据界面的 VoiceOver 读取体验。
- Bring accessibility to charts in your app — 为图表提供可访问的数据语义和导航方式。
- Focus on iPad keyboard navigation — 深入理解 iPadOS 的键盘焦点系统和 Tab 导航。
评论
GitHub Issues · utterances