WWDC Quick Look 💓 By SwiftGGTeam
Accessibility design for Mac Catalyst

Accessibility design for Mac Catalyst

观看原视频

Highlight

Mac Catalyst 会自动转换 iOS 无障碍信息,但开发者仍要补齐 Mac 上的键盘焦点、快捷键、语义分组标签,并用 Accessibility Inspector 验证 UIKit 无障碍属性在 macOS 上的映射。

核心内容

把一个已经可访问的 iPad App 带到 Mac,并不等于无障碍工作结束。UIKit 会把很多 iOS 无障碍信息转换到 Mac Catalyst,但用户在 Mac 上使用 App 的方式变了:键盘是主要输入设备,VoiceOver 依靠键盘在元素树中移动,桌面界面也常常比 iPad 上更密集。

Session 用 Roasted Beans 这个咖啡示例 App 展开。这个 App 已经在 iOS 上做过无障碍适配,现在要通过 Mac Catalyst 运行在 macOS。刚开始,UIKit 已经能让按钮、侧边栏和工具按钮进入 Tab 焦点循环;如果用户按方向键切换 table view 选项,有些列表还需要开发者显式开启新的 selectionFollowsFocus API。

键盘只是第一层。Mac 用户还期待常用动作有快捷键,例如分享、添加、评分。对于依赖辅助技术的用户,反复寻找屏幕上的按钮会增加负担。把高频操作放进菜单栏和快捷键,可以减少导航成本,也能回馈到 iOS 的 Full Keyboard Access。

第二层是 VoiceOver 导航效率。iOS 上用户可以用触摸快速跳到屏幕上的某个元素;Mac 上没有这个捷径,VoiceOver 用户要用键盘穿过 accessibility tree。如果一个界面暴露 26 个可访问元素,用户可能要按很多次键才能到达目标。accessibilityContainerType 让相关元素形成分组,Mac Catalyst 会把容器当作树上的独立节点,用户可以先跳过整个组,再决定是否进入组内细节。

最后是测试。开发者仍然写 UIKit 无障碍 API,但 App 运行在 macOS 上。WWDC 2020 的 Accessibility Inspector 新增 Catalyst 区域,会显示 iOS 的 traits、container type、automation type 和 UIKit 视图控制器信息。它能帮助开发者确认转换后的 Mac 无障碍树是否符合预期。

详细内容

1. 让列表选择跟随键盘焦点

03:58)UIKit 默认让很多控件参与键盘焦点循环。对于 UITableViewUICollectionView,如果方向键没有同步改变选择,Session 给出的入口是 selectionFollowsFocus

myTableView.selectionFollowsFocus = true

关键点:

  • selectionFollowsFocus 让方向键移动焦点时自动更新 table view 或 collection view 的选择。
  • UISplitView 侧边栏里的 table view 会由 UIKit 自动开启这个行为。
  • 普通列表需要开发者根据界面意图手动开启。
  • 这个设置解决的是 Mac 上“焦点到了,选择没跟上”的断裂感。

2. 把高频动作接入菜单栏和快捷键

05:56)示例 App 希望给分享操作添加 Command-I。实现路径是覆写 AppDelegate.buildMenu(with:),创建一个 UIKeyCommand,再把它作为 UIMenu 的 child 插入菜单栏。

extension AppDelegate {
  override func buildMenu(with builder: UIMenuBuilder) {
    super.buildMenu(with: builder)
    let shareCommand = UIKeyCommand(title: NSLocalizedString("Share", comment: ""),
                                    action: #selector(Self.handleShareMenuAction),
                                    input: "I",
                                    modifierFlags: [.command])
    let shareMenu = UIMenu(title: "",
                           identifier: UIMenu.Identifier("com.example.apple-samplecode.RoastedBeans.share"),
                           options: .displayInline,
                           children: [shareCommand])
    builder.insertChild(shareMenu, atEndOfMenu: .edit)
  }

  @objc func handleShareMenuAction() {
  }
}

关键点:

  • buildMenu(with:) 是 Mac Catalyst 菜单栏定制入口。
  • UIKeyCommandtitle 会显示在菜单项里,所以要使用本地化字符串。
  • input: "I"modifierFlags: [.command] 对应 Command-I。
  • action 指向实际处理分享的 selector。
  • UIMenu 使用 .displayInline,让单个分享命令直接显示在目标菜单位置。
  • builder.insertChild(..., atEndOfMenu: .edit) 把命令放到 Edit 菜单末尾。

07:16)Session 还提到 iOS 13.4 起可以从 UIPress 读取原始 key code。游戏或更底层的键盘控制可以直接响应具体按键。

extension MyViewController {
  override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
    switch presses.first?.key?.keyCode {
    case .keyboardLeftGUI:
      // Handle command key pressed
    case .keyboardB:
      // Handle B key pressed
    default:
    }
  }
}

关键点:

  • pressesBegan(_:with:) 在按键按下时进入 responder。
  • presses.first?.key?.keyCode 可以读取具体键位。
  • .keyboardLeftGUI 对应左 Command 键。
  • .keyboardB 对应 B 键。
  • 这类原始事件适合游戏或需要完整键盘控制权的界面;普通菜单命令优先使用 UIKeyCommand

3. 用 accessibilityContainerType 降低 VoiceOver 导航成本

09:00)VoiceOver 会根据 accessibility tree 读取界面。iOS 模型里,可访问元素通常是一层 leaf node;Mac 上用户用键盘逐项移动,复杂界面会暴露太多连续节点。Session 的 Roasted Beans 示例里,分组前有 26 个可见可访问元素。

10:49accessibilityContainerType 让元素之间建立关系。Mac Catalyst 会把容器本身放进 accessibility tree,容器里的元素成为子树。用户可以先聚焦容器,选择跳过整个容器,或进入容器内部继续导航。

15:28)标准 UITableViewUICollectionView 默认已经是 semanticGroup。开发者需要给这些容器补上清晰的本地化标签。

tableView.accessibilityLabel = NSLocalizedString("Coffee list", comment: "")

关键点:

  • UITableView 本身已经是一个语义分组容器。
  • Mac Catalyst 会让容器成为可聚焦的独立节点。
  • 容器需要有自己的 accessibilityLabel,否则 VoiceOver 聚焦容器时缺少上下文。
  • 标签应该描述这个组的内容,例如 “Coffee list”。

15:50)如果容器带有状态,标签还应该包含关键状态。示例在选择某个咖啡后,把当前品牌名写进 table view 的可访问标签。

extension RBListViewController {

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let data = tableData[indexPath.row]
        let label = NSLocalizedString("Coffee list", comment: "")
        let selectedLabel = NSLocalizedString("%@ selected", comment: "")
        tableView.accessibilityLabel = label + ", " + String(format: selectedLabel, data.coffee.brand)
    }

}

关键点:

  • didSelectRowAt 是更新容器状态标签的合适位置。
  • label 描述容器是什么。
  • selectedLabel 描述容器当前选择了什么。
  • String(format: selectedLabel, data.coffee.brand) 把业务状态接进可访问文案。
  • 这样 VoiceOver 用户聚焦列表容器时,能先听到列表名称和当前选择。

4. 手动把自定义信息区做成语义分组

16:21)Roasted Beans 右侧详情区不是 table view,也不是 collection view。可用地点被实现成一个垂直 UIStackView,里面有多个 UILabel。如果保持原状,VoiceOver 用户要逐个穿过这些标签。Session 用 accessibilityContainerType = .semanticGroup 把它变成一个可跳过或可进入的组。

let stackView = UIStackView()
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false

let locationsAvailable = viewModel.locationsAvailable

let titleLabel = UILabel()
titleLabel.font = UIFont.preferredFont(forTextStyle: .body).bold()
titleLabel.text = NSLocalizedString("Availability: ", comment: "")
stackView.addArrangedSubview(titleLabel)

for location in locationsAvailable {
  let label = UILabel()
  label.font = UIFont.preferredFont(forTextStyle: .body)
  label.text = "• " + location
  label.accessibilityLabel = location
  stackView.addArrangedSubview(label)
}

stackView.accessibilityLabel = String(format: NSLocalizedString("Available at %@ locations", comment: ""), String(locationsAvailable.count))
stackView.accessibilityContainerType = .semanticGroup

关键点:

  • UIStackView 负责承载一组视觉上相关的可用地点。
  • 每个 UILabel 仍然有自己的 accessibilityLabel,进入组后可以逐项读取。
  • stackView.accessibilityLabel 给整个组一个摘要。
  • 摘要使用地点数量,避免用户先听完所有地点才知道这个区域的规模。
  • .semanticGroup 是 iOS 上通用的容器类型;Mac Catalyst 会把它映射成可聚焦的分组节点。
  • 分组过多会让内部元素变难发现,所以适合功能区、同类项目或口头描述时自然会归为一组的区域。

5. 用 Accessibility Inspector 检查 Catalyst 映射

18:48)Session 最后演示了 Accessibility Inspector。Mac Catalyst App 运行在 macOS 上,但开发者写的是 UIKit 无障碍 API。新版 Inspector 会在 Catalyst 区域显示 iOS API 侧的信息。

检查 cell 时,Inspector 能显示元素 label 和 value。继续用 Command-Control-Up 检查父级时,它会定位到 table view 容器,显示容器类型、UIKit 类转换后的 Mac 平台元素、所在 view controller,以及 automation type。开发者可以用这些信息确认 accessibilityLabelaccessibilityContainerType 是否真的进入了 macOS 无障碍树。

核心启发

  • 做什么:给 Mac Catalyst App 做一次键盘焦点审计。 为什么值得做:Session 明确说 macOS 上键盘是主要交互方式,所有可交互控件都应该能获得键盘焦点。 怎么开始:在系统设置中开启“Use keyboard navigation to move focus between controls”,用 Tab、方向键和空格遍历核心页面;对没有随焦点改变选择的 UITableViewUICollectionView 设置 selectionFollowsFocus = true

  • 做什么:把高频操作做成菜单命令和键盘快捷键。 为什么值得做:对辅助技术用户来说,寻找屏幕按钮会拖慢操作;快捷键能让分享、添加、评分这类动作直接触达。 怎么开始:在 AppDelegate.buildMenu(with:) 中创建 UIKeyCommand,设置本地化 titleactioninputmodifierFlags,再通过 UIMenuBuilder 插入合适的菜单。

  • 做什么:为复杂详情页设计 accessibility group。 为什么值得做:Mac 上 VoiceOver 用户用键盘逐项导航,分组可以把长列表式的 accessibility tree 变成更接近 AppKit 的层级树。 怎么开始:按功能区识别自然分组,例如详情摘要、可用地点、过滤条件;给容器设置本地化 accessibilityLabel,再把 accessibilityContainerType 设为 .semanticGroup

  • 做什么:让容器标签包含当前状态。 为什么值得做:容器在 Mac Catalyst 中会成为独立可聚焦节点,用户聚焦时需要听到组名和关键状态。 怎么开始:在选择变化、筛选变化或数据刷新后更新容器的 accessibilityLabel,例如把列表名称和当前选中项拼成一个本地化句子。

  • 做什么:把 Accessibility Inspector 加进 Mac Catalyst 回归测试。 为什么值得做:Inspector 能显示 Catalyst 映射后的 traits、container type、automation type 和 UIKit view controller,适合确认 UIKit 无障碍 API 在 macOS 上的实际结果。 怎么开始:针对核心页面检查子元素和父容器,确认 cell 有 label/value,table view 或自定义容器有 label 与 semanticGroup,并记录 automation type 供 UI 测试定位使用。

关联 Session

评论

GitHub Issues · utterances