构建和审计 SwiftUI、UIKit 和 AppKit 的无障碍功能,涵盖 VoiceOver、语音控制、开关控制、全键盘访问、动态类型、焦点恢复、标签/特征/操作、遍历、自定义转子、NSAccessibility、XCTest 检查、自适应系统偏好以及 App Store 无障碍声明。在实现无障碍 UI、修复无障碍审计问题、测试辅助技术行为或为 App Store 无障碍营养标签提供依据时使用。
iOS/macOS 无障碍 - SwiftUI、UIKit 和 AppKit
构建 SwiftUI、UIKit 和 AppKit 界面,使其能够与 VoiceOver、开关控制、语音控制、全键盘访问以及自适应无障碍设置配合使用。
目录
- 核心原则
- VoiceOver 如何读取元素
- SwiftUI 无障碍修饰符
- 焦点管理
- 动态类型
- 自定义转子
- 系统无障碍偏好
- 装饰性内容
- 语音控制
- 开关控制
- 全键盘访问
- 辅助访问 (iOS 18+)
- UIKit 无障碍模式
- AppKit 无障碍模式
- 无障碍自定义内容
- App Store 无障碍营养标签
- 测试无障碍功能
- 常见错误
- 审查清单
- 参考资料
核心原则
- 优先使用语义化的系统控件;自定义控件必须暴露相同的名称、角色、值、状态和操作。
- 确保每个任务都能通过辅助输入完成:不要让手势、颜色、动效、悬停状态或视觉布局成为理解或操作的唯一途径。
- 在导航、呈现、更新和关闭过程中,保持焦点和遍历的有意性。
- 让文本、布局、对比度、透明度和动效适应用户的无障碍设置。
- 将 App Store 无障碍声明视为有证据支持的产品声明,而非孤立控件级别支持的总结。
VoiceOver 如何读取元素
VoiceOver 按固定且不可配置的顺序读取元素属性:
标签 -> 值 -> 特征 -> 提示
设计标签、值和提示时,请考虑此阅读顺序。
SwiftUI 无障碍修饰符
有关详细的 SwiftUI 修饰符示例(标签、提示、特征、分组、自定义控件、可调整操作和自定义操作),请参阅 references/a11y-patterns.md。
焦点管理
焦点管理是大多数应用失败的地方。当表单、弹窗或弹出框关闭时,VoiceOver 焦点必须返回到触发它的元素。
本节涉及辅助技术的无障碍焦点。有关键盘焦点、方向焦点、focusSection()、场景焦点值和 UIFocusGuide,请使用 focus-engine 技能。
在遍历顺序中审计元素顺序和分组;将键盘焦点机制路由到 focus-engine。
@AccessibilityFocusState (iOS 15+)
@AccessibilityFocusState 是一个属性包装器,用于读取和写入当前的无障碍焦点。它适用于单目标焦点的 Bool 或多目标焦点的可选 Hashable 枚举。
struct ContentView: View {
@State private var showSheet = false
@AccessibilityFocusState private var focusOnTrigger: Bool
var body: some View {
Button("打开设置") { showSheet = true }
.accessibilityFocused($focusOnTrigger)
.sheet(isPresented: $showSheet) {
SettingsSheet()
.onDisappear {
// 稍作延迟,让过渡完成后再移动焦点
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
focusOnTrigger = true
}
}
}
}
}
使用枚举的多目标焦点
enum A11yFocus: Hashable {
case nameField
case emailField
case submitButton
}
struct FormView: View {
@AccessibilityFocusState private var focus: A11yFocus?
var body: some View {
Form {
TextField("姓名", text: $name)
.accessibilityFocused($focus, equals: .nameField)
TextField("邮箱", text: $email)
.accessibilityFocused($focus, equals: .emailField)
Button("提交") { validate() }
.accessibilityFocused($focus, equals: .submitButton)
}
}
func validate() {
if name.isEmpty {
focus = .nameField // 将 VoiceOver 焦点移到无效字段
}
}
}
自定义模态视图
自定义覆盖视图需要 .isModal 特征来限制 VoiceOver 焦点,并提供一个退出操作以供关闭:
CustomDialog()
.accessibilityAddTraits(.isModal)
.accessibilityAction(.escape) { dismiss() }
测试关闭作为模态契约的一部分:用户必须能够通过相关的辅助技术退出手势或键盘退出路径关闭覆盖视图,并且焦点应返回到触发元素或下一个逻辑目标。
无障碍通知 (UIKit)
当需要在 UIKit 上下文中声明式地通知更改或移动焦点时:
// 宣布状态更改(例如“项目已删除”、“上传完成”)
UIAccessibility.post(notification: .announcement, argument: "上传完成")
// 部分屏幕更新——将焦点移到特定元素
UIAccessibility.post(notification: .layoutChanged, argument: targetView)
// 全屏过渡——将焦点移到新屏幕
UIAccessibility.post(notification: .screenChanged, argument: newScreenView)
动态类型
使用系统文本样式缩放文本。同时缩放非文本尺寸:图标大小、间距、控件高度和自定义点击区域尺寸应使用 @ScaledMetric(relativeTo:),以便它们需要跟踪文本大小。
有关动态类型和自适应布局的示例,包括 @ScaledMetric 和最小点击目标模式,请参阅 references/a11y-patterns.md。
自定义转子
转子让 VoiceOver 用户快速导航到特定内容类型。为内容密集的屏幕添加自定义转子。有关完整的转子示例,请参阅 references/a11y-patterns.md。
系统无障碍偏好
始终尊重这些环境值:
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var contrast // .standard 或 .increased
@Environment(\.legibilityWeight) var legibilityWeight // .regular 或 .bold
减少动效
用淡入淡出或无动画替换基于运动的动画:
withAnimation(reduceMotion ? nil : .spring()) {
showContent.toggle()
}
content.transition(reduceMotion ? .opacity : .slide)
检查每一个移动过渡,包括行删除、数量变化、表单或结账呈现以及模态关闭。在减少动效模式下,用透明度变化、即时状态变化或无动画替换滑动、弹跳、视差、弹簧和大型空间过渡。
减少透明度、增加对比度、粗体文本
// 减少透明度时使用纯色背景
.background(reduceTransparency ? Color(.systemBackground) : Color(.systemBackground).opacity(0.85))
// 增加对比度时使用更强的颜色
.foregroundStyle(contrast == .increased ? .primary : .secondary)
// 系统粗体文本启用时使用粗体字重
.fontWeight(legibilityWeight == .bold ? .bold : .regular)
装饰性内容
// 装饰性图片:对 VoiceOver 隐藏
Image(decorative: "background-pattern")
Image("visual-divider").accessibilityHidden(true)
// 图标旁边的文本:Label 自动处理
Label("设置", systemImage: "gear")
// 仅图标按钮:必须有无障碍标签
Button(action: { }) {
Image(systemName: "gear")
}
.accessibilityLabel("设置")
仅当图片不提供超出相邻可访问文本的信息时,才将其视为装饰性。如果它传达了产品变体、状态、图表点、用户生成内容或其他区分细节,请提供有意义的描述而不是隐藏它。
语音控制
语音控制依赖无障碍标签来生成语音点击目标。如果标签缺失或无法说出,语音控制无法定位该元素。
- 每个交互元素必须有一个可说的无障碍标签(不能仅使用表情符号或符号)。
- 标签在可见屏幕内必须唯一——重复的标签会迫使用户通过覆盖编号来区分。
- 将
accessibilityInputLabels视为预冻结的无障碍工作,用于长、笨拙、本地化、缩写多或常缩短的语音标签;不要将其推迟为润色。语音控制和全键盘访问使用这些标签。按重要性降序列出替代项。 - 广泛地将
accessibilityInputLabels应用于任何主标签难以说出的可见目标,包括重复的行操作、数量控件、账户/设置链接、媒体控件以及带有缩写或产品名称的本地化标签。 - 在启用语音控制的情况下测试:说“显示名称”和“显示编号”以验证所有交互元素都可定位。
- 对于语音控制审查,验证两种覆盖:“显示名称”确认可说的标签,“显示编号”确认当名称缺失、重复或笨拙时,每个可见的交互目标仍然可达。
有关 accessibilityInputLabels 示例和可说标签指南,请参阅 references/a11y-patterns.md。
开关控制
开关控制按阅读顺序顺序扫描无障碍元素。正确的分组和自定义操作对可用性至关重要。
- 使用
.accessibilityElement(children: .combine)对相关内容进行分组,以减少扫描停止点。 - 每个扫描目标应有意义且可操作。对 VoiceOver 隐藏的装饰性元素也对开关控制隐藏。
- 开关控制用户无法执行滑动删除、长按或多指手势。将这些交互作为
.accessibilityAction(named:)自定义操作暴露——开关控制会将其显示为菜单。 - 具有非标准点击区域的自定义控件应确保
accessibilityFrame准确反映可点击区域(用于点扫描模式)。
有关自定义操作和分组示例,请参阅 references/a11y-patterns.md。
全键盘访问
全键盘访问(iOS/iPadOS 13.4+)让用户使用硬件键盘导航和操作应用。
审计每个控件是否可达、有标签、可见焦点以及无需触摸即可操作。将 Tab/方向焦点、.focusable()、@FocusState、focusSection()、场景焦点值、tvOS 焦点和 UIFocusGuide 实现路由到 focus-engine。
- 每个交互元素都可以通过键盘到达和激活。
- 遍历顺序合乎逻辑,不会困住焦点。
- 焦点指示器在所有对比度和文本大小设置下保持可见。
- 仅手势行为有键盘可操作的替代方案。
- 应用快捷键不会覆盖系统定义的快捷键,如 Cmd+C、Cmd+V 或 Cmd+Tab。
有关全键盘访问审计检查,请参阅 references/a11y-patterns.md。
遍历顺序
元素顺序和分组必须遵循 VoiceOver 滑动顺序、开关控制扫描、语音控制覆盖和全键盘访问审查中的视觉和任务顺序。检查缺失或重复的标签、过多的行子元素、隐藏的自定义控件、焦点陷阱以及顺序与任务不符的分组。将键盘或方向路由机制保留在 focus-engine 中。
辅助访问 (iOS 18+)
辅助访问为认知障碍用户提供简化界面。应用应支持此模式:
// 检查辅助访问是否激活 (iOS 18+)
@Environment(\.accessibilityAssistiveAccessEnabled) var isAssistiveAccessEnabled
var body: some View {
if isAssistiveAccessEnabled {
SimplifiedContentView()
} else {
FullContentView()
}
}
关键指南:
- 减少视觉复杂性:更少的控件、更大的点击目标、更简单的导航
- 对标签和说明使用清晰、字面化的语言
- 最小化一次呈现的选择数量
- 在设置 > 辅助功能 > 辅助访问中启用辅助访问进行测试
UIKit 无障碍模式
对于自定义 UIKit 视图,暴露有意义的元素、标签、值、特征和操作;使用 insert/remove 改变特征;使自定义覆盖视图成为模态;并使用适当的公告、布局更改或屏幕更改通知。加载 references/a11y-patterns.md 获取完整的 UIKit 示例。
AppKit 无障碍模式
优先使用标准的 AppKit 控件。对于自定义 NSView 或虚拟元素,暴露正确的 NSAccessibility 角色、标签、值、操作和状态更改通知。加载 references/a11y-patterns.md 获取 NSAccessibilityElement 和自定义控件示例。
无障碍自定义内容
使用 .accessibilityCustomContent 提供有用的次要信息,而无需增加滑动停止点;为应自动读取的内容保留高重要性。有关 SwiftUI、UIKit 和 AppKit 示例,请参阅 references/a11y-patterns.md。
App Store 无障碍营养标签
有关 App Store 无障碍营养标签、产品页面声明或 App Store Connect 无障碍回答,请阅读 references/nutrition-labels.md。
在推荐声明之前,要求有证据表明用户可以在相关设备类型上使用该功能完成所有常见任务。使用结构化的常见任务按无障碍功能矩阵,包括当音频内容需要字幕时的媒体转录,并明确警告 App Store 无障碍回答必须保持准确,不得视为营销声明。
测试无障碍功能
手动测试
- 无障碍检查器:审计标签、特征和对比度,针对模拟器和设备。
- VoiceOver 测试:在设置 > 辅助功能 > VoiceOver 中启用。使用滑动手势导航每个屏幕。
- 语音控制测试:在设置 > 辅助功能 > 语音控制中启用。同时说“显示名称”和“显示编号”;名称验证可说的标签,而编号验证即使名称重复、缺失或笨拙时,每个可见的交互目标仍然可达。
- 全键盘访问测试:在设置 > 辅助功能 > 键盘 > 全键盘访问中启用。Tab 遍历每个屏幕,验证所有交互元素都获得焦点。
- 开关控制测试:在设置 > 辅助功能 > 开关控制中启用。验证扫描顺序合乎逻辑,并且基于手势的交互出现自定义操作。
- 动态类型:在设置 > 辅助功能 > 显示与文字大小 > 更大文字中测试所有文本大小。
使用 XCTest 进行自动化测试
使用稳定的无障碍标识符定位 XCUIElement 值,然后断言存在性、启用/选中状态、有意义的标签/值,以及测试环境暴露焦点时的 hasFocus。涵盖关闭焦点恢复、每个模态退出路径和手势替代方案。UI 自动化补充——而非替代——VoiceOver、语音控制、开关控制、键盘、动态类型、对比度、减少动效和减少透明度测试。加载 references/a11y-patterns.md 获取 XCTest 示例。
常见错误
| 错误 | 修复 |
|---|---|
| 特征分配覆盖行为 | 插入/移除 UIKit 特征或使用 SwiftUI 无障碍特征修饰符。 |
| 关闭后丢失焦点 | 将无障碍焦点返回到触发元素。 |
| 行创建过多滑动停止点 | 有意地分组相关子元素。 |
| 标签重复控件类型或省略图标含义 | 使用简洁、可说的操作/名称;特征宣布类型。 |
| 动效、文本大小、对比度或透明度固定 | 响应匹配的无障碍偏好和自适应文本样式。 |
| 目标小或仅颜色 | 提供 44×44 的目标以及文本、形状或图标语义。 |
| 自定义覆盖视图不是模态 | 暴露模态语义、退出操作和恢复。 |
审查清单
对于每个常见任务,记录以下门控的证据:
- [ ] 语义:控件暴露简洁的名称、正确的角色/状态/值,以及装饰性、仅颜色、仅手势或可调整内容的替代方案。
- [ ] 导航:分组和遍历是有意的;模态暴露模态和退出行为;焦点返回到发起控件。
- [ ] 输入:语音控制名称可说出且唯一,显示名称/编号均有效,开关控制暴露手势替代方案,全键盘访问没有不可达的控件、陷阱或覆盖的系统快捷键。
- [ ] 自适应:动态类型、减少动效、减少透明度、增加对比度、粗体文本和 44x44 点目标在代表性布局和状态下得到验证。
- [ ] 自动化:XCTest 覆盖稳定的标识符、状态、焦点(如果可用)以及每个模态退出路径,而不替代手动辅助技术测试。
- [ ] 并发:跨越隔离边界的无障碍值和通知负载是
Sendable的。 - [ ] 声明:每个 App Store 无障碍声明都得到针对每个声明设备类型的已完成常见任务证据矩阵的支持。
参考资料
- references/a11y-patterns.md — SwiftUI 和 UIKit 修饰符示例、分组、自定义操作、转子、动态类型
- references/nutrition-labels.md — App Store 无障碍营养标签:当前类别及通过/失败标准
- references/media-accessibility.md — 字幕、音频描述、AVMediaCharacteristic、SDH






