ios-accessibility
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseiOS Accessibility — SwiftUI and UIKit
iOS无障碍开发 — SwiftUI与UIKit
Every user-facing view must be usable with VoiceOver, Switch Control, Voice Control, Full Keyboard Access, and other assistive technologies. This skill covers the patterns and APIs required to build accessible iOS apps.
所有用户可见视图必须支持VoiceOver、切换控制(Switch Control)、语音控制(Voice Control)、全键盘访问(Full Keyboard Access)等辅助技术。本技能涵盖构建无障碍iOS应用所需的开发模式和API。
Core Principles
核心原则
- Every interactive element MUST have an accessible label. If no visible text exists, add .
.accessibilityLabel - Every custom control MUST have correct traits via (never direct assignment).
.accessibilityAddTraits - Decorative images MUST be hidden from assistive technologies.
- Sheet and dialog dismissals MUST return VoiceOver focus to the trigger element.
- All tap targets MUST be at least 44x44 points.
- Dynamic Type MUST be supported everywhere (system fonts, , adaptive layouts).
@ScaledMetric - No information conveyed by color alone -- always provide text or icon alternatives.
- System accessibility preferences MUST be respected: Reduce Motion, Reduce Transparency, Bold Text, Increase Contrast.
- 所有可交互元素必须配备无障碍标签。如果没有可见文本,需添加。
.accessibilityLabel - 所有自定义控件必须通过设置正确的特征(禁止直接赋值)。
.accessibilityAddTraits - 装饰性图片必须对辅助技术隐藏。
- 底部弹窗和对话框关闭后,必须将VoiceOver焦点返还给触发元素。
- 所有点击区域尺寸不得小于44x44点。
- 所有场景必须支持动态字体(系统字体、、自适应布局)。
@ScaledMetric - 禁止仅通过颜色传递信息,必须同时提供文本或图标替代方案。
- 必须遵守系统无障碍偏好设置:减少动态效果、降低透明度、粗体文本、增强对比度。
How VoiceOver Reads Elements
VoiceOver读取元素的规则
VoiceOver reads element properties in a fixed, non-configurable order:
Label -> Value -> Trait -> Hint
Design your labels, values, and hints with this reading order in mind.
VoiceOver会按照固定、不可配置的顺序读取元素属性:
标签 -> 值 -> 特征 -> 提示
设计标签、值和提示时请遵循该读取顺序。
SwiftUI Accessibility Modifiers
SwiftUI无障碍修饰符
Labels, Values, and Hints
标签、值与提示
swift
// Label: the primary description VoiceOver reads
Button(action: { }) {
Image(systemName: "heart.fill")
}
.accessibilityLabel("Favorite")
// Hint: describes the result of activation (read after a pause)
Button("Submit")
.accessibilityHint("Submits the form and sends your feedback")
// Value: the current state for sliders, toggles, progress indicators
Slider(value: $volume, in: 0...100)
.accessibilityValue("\(Int(volume)) percent")- Label: Short, descriptive noun or noun phrase. Do not include the element type (VoiceOver announces the trait separately).
- Value: Current state. Update dynamically as the value changes.
- Hint: Starts with a verb, describes the result. Only add when the action is not obvious from the label and trait.
swift
// 标签:VoiceOver读取的核心描述
Button(action: { }) {
Image(systemName: "heart.fill")
}
.accessibilityLabel("Favorite")
// 提示:描述触发后的结果(停顿后读取)
Button("Submit")
.accessibilityHint("Submits the form and sends your feedback")
// 值:滑块、开关、进度指示器的当前状态
Slider(value: $volume, in: 0...100)
.accessibilityValue("\(Int(volume)) percent")- 标签:简短的描述性名词或名词短语,不要包含元素类型(VoiceOver会单独播报特征)。
- 值:当前状态,需随值变化动态更新。
- 提示:以动词开头,描述操作结果。仅当从标签和特征无法明确判断操作时才需要添加。
Input Labels
输入标签
Use to provide alternative labels for Voice Control:
accessibilityInputLabelsswift
Button("Go") { }
.accessibilityInputLabels(["Go", "Start", "Begin"])使用为语音控制提供备选标签:
accessibilityInputLabelsswift
Button("Go") { }
.accessibilityInputLabels(["Go", "Start", "Begin"])Traits
特征
Use and to modify traits. NEVER use direct trait assignment -- it overwrites the element's built-in traits.
.accessibilityAddTraits.accessibilityRemoveTraits| Trait | Use For |
|---|---|
| Custom tappable views that are not |
| Section headers (enables rotor heading navigation) |
| Elements that navigate to external content |
| Currently selected tab, segment, or radio button |
| Meaningful images |
| Custom toggle controls |
| Trap VoiceOver focus inside a custom overlay |
| Timers, live counters, real-time displays |
| Custom search inputs |
| Elements that begin audio/video playback |
swift
// WRONG: overwrites Button's built-in .isButton trait
Button("Go") { }
.accessibilityTraits(.updatesFrequently)
// CORRECT: adds to existing traits
Button("Go") { }
.accessibilityAddTraits(.updatesFrequently)
// Remove a trait when needed
Text("Not really a header anymore")
.accessibilityRemoveTraits(.isHeader)使用和修改特征。禁止直接赋值特征,否则会覆盖元素的内置特征。
.accessibilityAddTraits.accessibilityRemoveTraits| 特征 | 适用场景 |
|---|---|
| 非系统 |
| 分区标题(支持转子标题导航) |
| 跳转至外部内容的元素 |
| 当前选中的标签页、分段控件、单选按钮 |
| 有实际含义的图片 |
| 自定义开关控件 |
| 捕获VoiceOver焦点的自定义浮层 |
| 计时器、实时计数器、实时展示内容 |
| 自定义搜索输入框 |
| 触发音视频播放的元素 |
swift
// 错误写法:覆盖了Button内置的.isButton特征
Button("Go") { }
.accessibilityTraits(.updatesFrequently)
// 正确写法:在现有特征基础上新增
Button("Go") { }
.accessibilityAddTraits(.updatesFrequently)
// 按需移除特征
Text("Not really a header anymore")
.accessibilityRemoveTraits(.isHeader)Element Grouping
元素分组
Reduce VoiceOver swipe count by grouping related elements into a single accessibility stop.
swift
// .combine: merge children into one VoiceOver element
// VoiceOver concatenates child labels automatically
HStack {
Image(systemName: "person.circle")
VStack {
Text("John Doe")
Text("Engineer")
}
}
.accessibilityElement(children: .combine)
// .ignore: replace children with a completely custom label
HStack {
Image(systemName: "envelope")
Text("inbox@example.com")
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Email: inbox@example.com")
// .contain: keep children individually navigable but logically grouped
VStack {
Text("Order #1234")
Button("Track") { }
}
.accessibilityElement(children: .contain)List rows should use unless individual child elements require separate focus (e.g., a row with multiple independently interactive controls).
.accessibilityElement(children: .combine)将相关元素合并为单个无障碍停靠点,减少VoiceOver滑动次数。
swift
// .combine:将子元素合并为单个VoiceOver元素
// VoiceOver会自动拼接子元素的标签
HStack {
Image(systemName: "person.circle")
VStack {
Text("John Doe")
Text("Engineer")
}
}
.accessibilityElement(children: .combine)
// .ignore:完全替换子元素为自定义标签
HStack {
Image(systemName: "envelope")
Text("inbox@example.com")
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Email: inbox@example.com")
// .contain:保留子元素独立可导航性,但逻辑上归为一组
VStack {
Text("Order #1234")
Button("Track") { }
}
.accessibilityElement(children: .contain)列表行默认使用,除非子元素需要独立获取焦点(例如包含多个独立可交互控件的行)。
.accessibilityElement(children: .combine)Custom Controls with accessibilityRepresentation
基于accessibilityRepresentation的自定义控件
Use (iOS 15+) for custom controls that map to standard SwiftUI equivalents. The framework generates correct accessibility elements from the representation automatically, including traits, adjustable actions, and values.
.accessibilityRepresentationswift
HStack {
Text("Dark Mode")
Circle()
.fill(isDark ? .green : .gray)
.onTapGesture { isDark.toggle() }
}
.accessibilityRepresentation {
Toggle("Dark Mode", isOn: $isDark)
}swift
// Custom slider control
VStack {
SliderTrack(value: value) // Custom visual implementation
}
.accessibilityRepresentation {
Slider(value: $value, in: 0...100) {
Text("Volume")
}
}对等于标准SwiftUI组件的自定义控件,可使用(iOS 15+)。框架会自动基于定义的组件生成正确的无障碍元素,包括特征、可调整操作和值。
.accessibilityRepresentationswift
HStack {
Text("Dark Mode")
Circle()
.fill(isDark ? .green : .gray)
.onTapGesture { isDark.toggle() }
}
.accessibilityRepresentation {
Toggle("Dark Mode", isOn: $isDark)
}swift
// 自定义滑块控件
VStack {
SliderTrack(value: value) // 自定义视觉实现
}
.accessibilityRepresentation {
Slider(value: $value, in: 0...100) {
Text("Volume")
}
}Adjustable Controls
可调整控件
For controls that support increment/decrement (star ratings, steppers):
swift
HStack { /* custom star rating UI */ }
.accessibilityElement()
.accessibilityLabel("Rating")
.accessibilityValue("\(rating) out of 5 stars")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: if rating < 5 { rating += 1 }
case .decrement: if rating > 1 { rating -= 1 }
@unknown default: break
}
}VoiceOver users swipe up/down to adjust. The modifier automatically adds the trait.
.accessibilityAdjustableAction.adjustable适用于支持增减操作的控件(星级评分、步进器):
swift
HStack { /* 自定义星级评分UI */ }
.accessibilityElement()
.accessibilityLabel("Rating")
.accessibilityValue("\(rating) out of 5 stars")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: if rating < 5 { rating += 1 }
case .decrement: if rating > 1 { rating -= 1 }
@unknown default: break
}
}VoiceOver用户可上下滑动调整数值。修饰符会自动添加特征。
.accessibilityAdjustableAction.adjustableCustom Actions
自定义操作
Replace hidden swipe actions, context menus, or long-press gestures with named accessibility actions so VoiceOver users can discover and invoke them:
swift
MessageRow(message: message)
.accessibilityAction(named: "Reply") { reply(to: message) }
.accessibilityAction(named: "Delete") { delete(message) }
.accessibilityAction(named: "Flag") { flag(message) }System-level actions:
swift
PlayerView()
.accessibilityAction(.magicTap) { togglePlayPause() }
.accessibilityAction(.escape) { dismiss() }- Magic Tap (two-finger double-tap): Toggle the most relevant action (play/pause, answer/end call).
- Escape (two-finger Z-scrub): Dismiss the current modal or go back.
将隐藏的滑动操作、上下文菜单、长按手势替换为命名无障碍操作,方便VoiceOver用户发现和触发:
swift
MessageRow(message: message)
.accessibilityAction(named: "Reply") { reply(to: message) }
.accessibilityAction(named: "Delete") { delete(message) }
.accessibilityAction(named: "Flag") { flag(message) }系统级操作:
swift
PlayerView()
.accessibilityAction(.magicTap) { togglePlayPause() }
.accessibilityAction(.escape) { dismiss() }- Magic Tap(双指双击):触发最相关的核心操作(播放/暂停、接听/挂断电话)。
- Escape(双指Z形滑动):关闭当前弹窗或返回上一级。
Sort Priority
排序优先级
Control VoiceOver reading order among sibling elements. Higher values are read first:
swift
ZStack {
Image("photo").accessibilitySortPriority(0) // Read third
Text("Credit").accessibilitySortPriority(1) // Read second
Text("Breaking News").accessibilitySortPriority(2) // Read first
}Only use when the default visual order produces a confusing reading sequence.
控制同级元素的VoiceOver读取顺序,数值越高越先读取:
swift
ZStack {
Image("photo").accessibilitySortPriority(0) // 第三个读取
Text("Credit").accessibilitySortPriority(1) // 第二个读取
Text("Breaking News").accessibilitySortPriority(2) // 第一个读取
}仅当默认视觉顺序导致读取逻辑混乱时使用。
Focus Management
焦点管理
Focus management is where most apps fail. When a sheet, alert, or popover is dismissed, VoiceOver focus MUST return to the element that triggered it.
焦点管理是大多数应用的常见问题。当底部弹窗、警告框、弹出层关闭时,VoiceOver焦点必须返还给触发元素。
@AccessibilityFocusState (iOS 15+)
@AccessibilityFocusState(iOS 15+)
@AccessibilityFocusStateBoolHashableswift
struct ContentView: View {
@State private var showSheet = false
@AccessibilityFocusState private var focusOnTrigger: Bool
var body: some View {
Button("Open Settings") { showSheet = true }
.accessibilityFocused($focusOnTrigger)
.sheet(isPresented: $showSheet) {
SettingsSheet()
.onDisappear {
// Slight delay allows the transition to complete before moving focus
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
focusOnTrigger = true
}
}
}
}
}@AccessibilityFocusStateBoolHashableswift
struct ContentView: View {
@State private var showSheet = false
@AccessibilityFocusState private var focusOnTrigger: Bool
var body: some View {
Button("Open Settings") { showSheet = true }
.accessibilityFocused($focusOnTrigger)
.sheet(isPresented: $showSheet) {
SettingsSheet()
.onDisappear {
// 短暂延迟等待过渡动画完成后再移动焦点
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
focusOnTrigger = true
}
}
}
}
}Multi-Target Focus with Enum
基于枚举的多目标焦点
swift
enum A11yFocus: Hashable {
case nameField
case emailField
case submitButton
}
struct FormView: View {
@AccessibilityFocusState private var focus: A11yFocus?
var body: some View {
Form {
TextField("Name", text: $name)
.accessibilityFocused($focus, equals: .nameField)
TextField("Email", text: $email)
.accessibilityFocused($focus, equals: .emailField)
Button("Submit") { validate() }
.accessibilityFocused($focus, equals: .submitButton)
}
}
func validate() {
if name.isEmpty {
focus = .nameField // Move VoiceOver to the invalid field
}
}
}swift
enum A11yFocus: Hashable {
case nameField
case emailField
case submitButton
}
struct FormView: View {
@AccessibilityFocusState private var focus: A11yFocus?
var body: some View {
Form {
TextField("Name", text: $name)
.accessibilityFocused($focus, equals: .nameField)
TextField("Email", text: $email)
.accessibilityFocused($focus, equals: .emailField)
Button("Submit") { validate() }
.accessibilityFocused($focus, equals: .submitButton)
}
}
func validate() {
if name.isEmpty {
focus = .nameField // 将VoiceOver焦点移动到校验不通过的输入框
}
}
}Custom Modals
自定义弹窗
Custom overlay views need the trait to trap VoiceOver focus and an escape action for dismissal:
.isModalswift
CustomDialog()
.accessibilityAddTraits(.isModal)
.accessibilityAction(.escape) { dismiss() }自定义浮层视图需要添加特征捕获VoiceOver焦点,同时提供退出操作用于关闭:
.isModalswift
CustomDialog()
.accessibilityAddTraits(.isModal)
.accessibilityAction(.escape) { dismiss() }Accessibility Notifications (UIKit)
无障碍通知(UIKit)
When you need to announce changes or move focus imperatively in UIKit contexts:
swift
// Announce a status change (e.g., "Item deleted", "Upload complete")
UIAccessibility.post(notification: .announcement, argument: "Upload complete")
// Partial screen update -- move focus to a specific element
UIAccessibility.post(notification: .layoutChanged, argument: targetView)
// Full screen transition -- move focus to the new screen
UIAccessibility.post(notification: .screenChanged, argument: newScreenView)在UIKit场景中需要主动播报变更或移动焦点时使用:
swift
// 播报状态变更(例如"项目已删除"、"上传完成")
UIAccessibility.post(notification: .announcement, argument: "Upload complete")
// 局部屏幕更新:将焦点移动到指定元素
UIAccessibility.post(notification: .layoutChanged, argument: targetView)
// 全屏切换:将焦点移动到新页面
UIAccessibility.post(notification: .screenChanged, argument: newScreenView)Dynamic Type
动态字体
@ScaledMetric (iOS 14+)
@ScaledMetric(iOS 14+)
@ScaledMetricswift
@ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
@ScaledMetric private var spacing: CGFloat = 8
var body: some View {
HStack(spacing: spacing) {
Image(systemName: "star.fill")
.frame(width: iconSize, height: iconSize)
Text("Favorite")
}
}@ScaledMetricswift
@ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
@ScaledMetric private var spacing: CGFloat = 8
var body: some View {
HStack(spacing: spacing) {
Image(systemName: "star.fill")
.frame(width: iconSize, height: iconSize)
Text("Favorite")
}
}Adaptive Layouts
自适应布局
Switch from horizontal to vertical layout at large accessibility text sizes:
swift
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if dynamicTypeSize.isAccessibilitySize {
VStack(alignment: .leading) { icon; textContent }
} else {
HStack { icon; textContent }
}
}Use (iOS 15+) rather than comparing against specific cases -- it covers all five accessibility size categories.
dynamicTypeSize.isAccessibilitySize在大字号无障碍模式下从横向布局切换为纵向布局:
swift
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if dynamicTypeSize.isAccessibilitySize {
VStack(alignment: .leading) { icon; textContent }
} else {
HStack { icon; textContent }
}
}优先使用(iOS 15+)而不是比对具体字号,它覆盖了所有五个无障碍字号分类。
dynamicTypeSize.isAccessibilitySizeMinimum Tap Targets
最小点击区域
Every tappable element must be at least 44x44 points:
swift
Button(action: { }) {
Image(systemName: "plus")
.frame(minWidth: 44, minHeight: 44)
}
.contentShape(Rectangle())所有可点击元素尺寸必须至少为44x44点:
swift
Button(action: { }) {
Image(systemName: "plus")
.frame(minWidth: 44, minHeight: 44)
}
.contentShape(Rectangle())Custom Rotors
自定义转子
Rotors let VoiceOver users quickly navigate to specific content types. Add custom rotors for content-heavy screens:
swift
List(items) { item in
ItemRow(item: item)
}
.accessibilityRotor("Unread") {
ForEach(items.filter { !$0.isRead }) { item in
AccessibilityRotorEntry(item.title, id: item.id)
}
}
.accessibilityRotor("Flagged") {
ForEach(items.filter { $0.isFlagged }) { item in
AccessibilityRotorEntry(item.title, id: item.id)
}
}Users access rotors by rotating two fingers on screen. The system provides built-in rotors for headings, links, and form controls. Custom rotors extend this with app-specific navigation.
转子允许VoiceOver用户快速导航到特定内容类型,内容密集的页面可添加自定义转子:
swift
List(items) { item in
ItemRow(item: item)
}
.accessibilityRotor("Unread") {
ForEach(items.filter { !$0.isRead }) { item in
AccessibilityRotorEntry(item.title, id: item.id)
}
}
.accessibilityRotor("Flagged") {
ForEach(items.filter { $0.isFlagged }) { item in
AccessibilityRotorEntry(item.title, id: item.id)
}
}用户可通过双指旋转屏幕调出转子。系统内置了标题、链接、表单控件等转子,自定义转子可扩展应用专属的导航能力。
System Accessibility Preferences
系统无障碍偏好
Always respect these environment values:
swift
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var contrast // .standard or .increased
@Environment(\.legibilityWeight) var legibilityWeight // .regular or .bold始终遵循以下环境变量:
swift
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var contrast // .standard 或 .increased
@Environment(\.legibilityWeight) var legibilityWeight // .regular 或 .boldReduce Motion
减少动态效果
Replace movement-based animations with crossfades or no animation:
swift
withAnimation(reduceMotion ? nil : .spring()) {
showContent.toggle()
}
content.transition(reduceMotion ? .opacity : .slide)将基于运动的动画替换为淡入淡出或无动画:
swift
withAnimation(reduceMotion ? nil : .spring()) {
showContent.toggle()
}
content.transition(reduceMotion ? .opacity : .slide)Reduce Transparency, Increase Contrast, Bold Text
降低透明度、增强对比度、粗体文本
swift
// Solid backgrounds when transparency is reduced
.background(reduceTransparency ? Color(.systemBackground) : Color(.systemBackground).opacity(0.85))
// Stronger colors when contrast is increased
.foregroundStyle(contrast == .increased ? .primary : .secondary)
// Bold weight when system bold text is enabled
.fontWeight(legibilityWeight == .bold ? .bold : .regular)swift
// 开启降低透明度时使用纯色背景
.background(reduceTransparency ? Color(.systemBackground) : Color(.systemBackground).opacity(0.85))
// 开启增强对比度时使用更深的颜色
.foregroundStyle(contrast == .increased ? .primary : .secondary)
// 开启系统粗体文本时使用字重加粗
.fontWeight(legibilityWeight == .bold ? .bold : .regular)Decorative Content
装饰性内容
swift
// Decorative images: hidden from VoiceOver
Image(decorative: "background-pattern")
Image("visual-divider").accessibilityHidden(true)
// Icon next to text: Label handles this automatically
Label("Settings", systemImage: "gear")
// Icon-only buttons: MUST have an accessibility label
Button(action: { }) {
Image(systemName: "gear")
}
.accessibilityLabel("Settings")swift
// 装饰性图片:对VoiceOver隐藏
Image(decorative: "background-pattern")
Image("visual-divider").accessibilityHidden(true)
// 文本旁的图标:Label会自动处理无障碍逻辑
Label("Settings", systemImage: "gear")
// 仅含图标的按钮:必须添加无障碍标签
Button(action: { }) {
Image(systemName: "gear")
}
.accessibilityLabel("Settings")Assistive Access (iOS 26+)
辅助访问(iOS 26+)
iOS 26 introduces for supporting Assistive Access mode in scenes. Use the scene modifier to provide simplified versions of your app's UI for users with cognitive disabilities. Test your app with Assistive Access enabled in Settings > Accessibility > Assistive Access.
AssistiveAccessAssistiveAccessiOS 26引入了,用于支持场景中的辅助访问模式。使用场景修饰器为认知障碍用户提供简化版应用UI。可在设置 > 无障碍 > 辅助访问中开启该功能测试应用。
AssistiveAccessAssistiveAccessUIKit Accessibility Patterns
UIKit无障碍开发模式
When working with UIKit views:
- Set on meaningful custom views.
isAccessibilityElement = true - Set on all interactive elements without visible text.
accessibilityLabel - Use and
.insert()for trait modification (not direct assignment)..remove() - Set on custom overlay views to trap focus.
accessibilityViewIsModal = true - Post for transient status messages.
.announcement - Post with a target view for partial screen updates.
.layoutChanged - Post for full screen transitions.
.screenChanged
swift
// UIKit trait modification
customButton.accessibilityTraits.insert(.button)
customButton.accessibilityTraits.remove(.staticText)
// Modal overlay
overlayView.accessibilityViewIsModal = true使用UIKit视图时需遵循以下规则:
- 对有实际含义的自定义视图设置。
isAccessibilityElement = true - 所有无可见文本的可交互元素需设置。
accessibilityLabel - 使用和
.insert()修改特征(不要直接赋值)。.remove() - 对自定义浮层视图设置捕获焦点。
accessibilityViewIsModal = true - 发送通知播报临时状态消息。
.announcement - 局部屏幕更新时发送通知并传入目标视图。
.layoutChanged - 全屏切换时发送通知。
.screenChanged
swift
// UIKit特征修改
customButton.accessibilityTraits.insert(.button)
customButton.accessibilityTraits.remove(.staticText)
// 模态浮层
overlayView.accessibilityViewIsModal = trueAccessibility Custom Content
无障碍自定义内容
Use for supplementary details that should not clutter the primary label. VoiceOver users access custom content via the "More Content" rotor:
accessibilityCustomContentswift
ProductRow(product: product)
.accessibilityCustomContent("Price", product.formattedPrice)
.accessibilityCustomContent("Rating", "\(product.rating) out of 5")
.accessibilityCustomContent(
"Availability",
product.inStock ? "In stock" : "Out of stock",
importance: .high // .high reads automatically with the element
)使用添加无需展示在主标签中的补充信息。VoiceOver用户可通过「更多内容」转子访问自定义内容:
accessibilityCustomContentswift
ProductRow(product: product)
.accessibilityCustomContent("Price", product.formattedPrice)
.accessibilityCustomContent("Rating", "\(product.rating) out of 5")
.accessibilityCustomContent(
"Availability",
product.inStock ? "In stock" : "Out of stock",
importance: .high // 高重要性内容会随元素自动播报
)Common Mistakes
常见错误
- Direct trait assignment: overwrites all existing traits. Use
.accessibilityTraits(.isButton)..accessibilityAddTraits(.isButton) - Missing focus restoration: Dismissing sheets without returning VoiceOver focus to the trigger element.
- Ungrouped list rows: Multiple text elements per row create excessive swipe stops. Use .
.accessibilityElement(children: .combine) - Redundant trait in labels: reads as "Settings button, button." Omit the type.
.accessibilityLabel("Settings button") - Missing labels on icon-only buttons: Every -only button MUST have
Image..accessibilityLabel - Ignoring Reduce Motion: Always check before movement animations.
accessibilityReduceMotion - Fixed font sizes: ignores Dynamic Type. Use
.font(.system(size: 16))or similar text styles..font(.body) - Small tap targets: Icons without and
frame(minWidth: 44, minHeight: 44)..contentShape() - Color as sole indicator: Red/green for error/success without text or icon alternatives.
- Missing on overlays: Custom modals without
.isModallet VoiceOver escape..accessibilityAddTraits(.isModal)
- 直接赋值特征:会覆盖所有现有特征,应使用
.accessibilityTraits(.isButton)。.accessibilityAddTraits(.isButton) - 缺失焦点恢复:关闭弹窗时未将VoiceOver焦点返还给触发元素。
- 列表行未分组:单行包含多个文本元素会导致过多滑动停靠点,应使用。
.accessibilityElement(children: .combine) - 标签包含冗余特征:会播报为「Settings button, button」,应省略元素类型。
.accessibilityLabel("Settings button") - 仅含图标的按钮缺失标签:所有仅含的按钮必须添加
Image。.accessibilityLabel - 忽略减少动态效果设置:添加运动动画前必须检查。
accessibilityReduceMotion - 固定字体大小:会忽略动态字体,应使用
.font(.system(size: 16))等系统文本样式。.font(.body) - 点击区域过小:图标未设置和
frame(minWidth: 44, minHeight: 44)。.contentShape() - 仅用颜色作为标识:用红绿表示错误/成功时未提供文本或图标替代方案。
- 浮层缺失特征:自定义弹窗未添加
.isModal会导致VoiceOver焦点逸出。.accessibilityAddTraits(.isModal)
Review Checklist
审核检查清单
For every user-facing view, verify:
- Every interactive element has an accessible label
- Custom controls use correct traits via
.accessibilityAddTraits - Decorative images are hidden (or
Image(decorative:)).accessibilityHidden(true) - List rows group content with
.accessibilityElement(children: .combine) - Sheets and dialogs return focus to the trigger on dismiss
- Custom overlays have trait and escape action
.isModal - All tap targets are at least 44x44 points
- Dynamic Type supported (, system fonts, adaptive layouts)
@ScaledMetric - Reduce Motion respected (no movement animations when enabled)
- Reduce Transparency respected (solid backgrounds when enabled)
- Increase Contrast respected (stronger foreground colors)
- No information conveyed by color alone
- Custom actions provided for swipe-to-reveal and context menu features
- Icon-only buttons have labels
- Heading traits set on section headers
- Custom accessibility types and notification payloads are when passed across concurrency boundaries
Sendable
所有用户可见视图需验证以下项:
- 所有可交互元素都配备无障碍标签
- 自定义控件通过设置了正确特征
.accessibilityAddTraits - 装饰性图片已隐藏(或
Image(decorative:)).accessibilityHidden(true) - 列表行通过分组内容
.accessibilityElement(children: .combine) - 底部弹窗和对话框关闭后焦点返还给触发元素
- 自定义浮层添加了特征和退出操作
.isModal - 所有点击区域尺寸至少为44x44点
- 支持动态字体(、系统字体、自适应布局)
@ScaledMetric - 遵循减少动态效果设置(开启时无运动动画)
- 遵循降低透明度设置(开启时使用纯色背景)
- 遵循增强对比度设置(使用更深的前景色)
- 无仅通过颜色传递的信息
- 滑动展示和上下文菜单功能提供了自定义无障碍操作
- 仅含图标的按钮配备了标签
- 分区标题设置了标题特征
- 跨并发边界传递的自定义无障碍类型和通知载荷符合要求
Sendable
Apple Documentation Reference
Apple文档参考
For the latest API details, use the MCP server when available:
apple-docs- with queries like "accessibility SwiftUI" or "VoiceOver"
searchAppleDocumentation - with paths like
fetchAppleDocumentationor/documentation/swiftui/view-accessibility/documentation/swiftui/scaledmetric - Apple's modifier reference covers labels, values, hints, actions, traits, focus, rotors, and custom content
如需最新API详情,可使用 MCP服务:
apple-docs- 调用,传入查询词如「accessibility SwiftUI」或「VoiceOver」
searchAppleDocumentation - 调用,传入路径如
fetchAppleDocumentation或/documentation/swiftui/view-accessibility/documentation/swiftui/scaledmetric - Apple的修饰符参考涵盖了标签、值、提示、操作、特征、焦点、转子和自定义内容等所有能力