ios-accessibility

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

iOS 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

核心原则

  1. Every interactive element MUST have an accessible label. If no visible text exists, add
    .accessibilityLabel
    .
  2. Every custom control MUST have correct traits via
    .accessibilityAddTraits
    (never direct assignment).
  3. Decorative images MUST be hidden from assistive technologies.
  4. Sheet and dialog dismissals MUST return VoiceOver focus to the trigger element.
  5. All tap targets MUST be at least 44x44 points.
  6. Dynamic Type MUST be supported everywhere (system fonts,
    @ScaledMetric
    , adaptive layouts).
  7. No information conveyed by color alone -- always provide text or icon alternatives.
  8. System accessibility preferences MUST be respected: Reduce Motion, Reduce Transparency, Bold Text, Increase Contrast.
  1. 所有可交互元素必须配备无障碍标签。如果没有可见文本,需添加
    .accessibilityLabel
  2. 所有自定义控件必须通过
    .accessibilityAddTraits
    设置正确的特征(禁止直接赋值)。
  3. 装饰性图片必须对辅助技术隐藏。
  4. 底部弹窗和对话框关闭后,必须将VoiceOver焦点返还给触发元素。
  5. 所有点击区域尺寸不得小于44x44点。
  6. 所有场景必须支持动态字体(系统字体、
    @ScaledMetric
    、自适应布局)。
  7. 禁止仅通过颜色传递信息,必须同时提供文本或图标替代方案。
  8. 必须遵守系统无障碍偏好设置:减少动态效果、降低透明度、粗体文本、增强对比度。

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
accessibilityInputLabels
to provide alternative labels for Voice Control:
swift
Button("Go") { }
    .accessibilityInputLabels(["Go", "Start", "Begin"])
使用
accessibilityInputLabels
为语音控制提供备选标签:
swift
Button("Go") { }
    .accessibilityInputLabels(["Go", "Start", "Begin"])

Traits

特征

Use
.accessibilityAddTraits
and
.accessibilityRemoveTraits
to modify traits. NEVER use direct trait assignment -- it overwrites the element's built-in traits.
TraitUse For
.isButton
Custom tappable views that are not
Button
.isHeader
Section headers (enables rotor heading navigation)
.isLink
Elements that navigate to external content
.isSelected
Currently selected tab, segment, or radio button
.isImage
Meaningful images
.isToggle
Custom toggle controls
.isModal
Trap VoiceOver focus inside a custom overlay
.updatesFrequently
Timers, live counters, real-time displays
.isSearchField
Custom search inputs
.startsMediaSession
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
修改特征。禁止直接赋值特征,否则会覆盖元素的内置特征。
特征适用场景
.isButton
非系统
Button
的自定义可点击视图
.isHeader
分区标题(支持转子标题导航)
.isLink
跳转至外部内容的元素
.isSelected
当前选中的标签页、分段控件、单选按钮
.isImage
有实际含义的图片
.isToggle
自定义开关控件
.isModal
捕获VoiceOver焦点的自定义浮层
.updatesFrequently
计时器、实时计数器、实时展示内容
.isSearchField
自定义搜索输入框
.startsMediaSession
触发音视频播放的元素
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
.accessibilityElement(children: .combine)
unless individual child elements require separate focus
(e.g., a row with multiple independently interactive controls).
将相关元素合并为单个无障碍停靠点,减少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
.accessibilityRepresentation
(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.
swift
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组件的自定义控件,可使用
.accessibilityRepresentation
(iOS 15+)。框架会自动基于定义的组件生成正确的无障碍元素,包括特征、可调整操作和值。
swift
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
.accessibilityAdjustableAction
modifier automatically adds the
.adjustable
trait.
适用于支持增减操作的控件(星级评分、步进器):
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
修饰符会自动添加
.adjustable
特征。

Custom 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+)

@AccessibilityFocusState
is a property wrapper that reads and writes the current accessibility focus. It works with
Bool
for single-target focus or an optional
Hashable
enum for multi-target focus.
swift
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
                        }
                    }
            }
    }
}
@AccessibilityFocusState
是用于读取和写入当前无障碍焦点的属性包装器。单目标焦点可搭配
Bool
使用,多目标焦点可搭配可选
Hashable
枚举使用。
swift
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
.isModal
trait to trap VoiceOver focus and an escape action for dismissal:
swift
CustomDialog()
    .accessibilityAddTraits(.isModal)
    .accessibilityAction(.escape) { dismiss() }
自定义浮层视图需要添加
.isModal
特征捕获VoiceOver焦点,同时提供退出操作用于关闭:
swift
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+)

@ScaledMetric
scales numeric values (spacing, icon sizes, padding) proportionally with the user's preferred text size. It takes a base value and optionally a text style to scale relative to.
swift
@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")
    }
}
@ScaledMetric
会根据用户偏好的文本大小按比例缩放数值(间距、图标尺寸、内边距)。可传入基础值,也可选择关联的文本样式作为缩放基准。
swift
@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
dynamicTypeSize.isAccessibilitySize
(iOS 15+) rather than comparing against specific cases -- it covers all five accessibility size categories.
在大字号无障碍模式下从横向布局切换为纵向布局:
swift
@Environment(\.dynamicTypeSize) var dynamicTypeSize

var body: some View {
    if dynamicTypeSize.isAccessibilitySize {
        VStack(alignment: .leading) { icon; textContent }
    } else {
        HStack { icon; textContent }
    }
}
优先使用
dynamicTypeSize.isAccessibilitySize
(iOS 15+)而不是比对具体字号,它覆盖了所有五个无障碍字号分类。

Minimum 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 或 .bold

Reduce 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
AssistiveAccess
for supporting Assistive Access mode in scenes. Use the
AssistiveAccess
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.
iOS 26引入了
AssistiveAccess
,用于支持场景中的辅助访问模式。使用
AssistiveAccess
场景修饰器为认知障碍用户提供简化版应用UI。可在设置 > 无障碍 > 辅助访问中开启该功能测试应用。

UIKit Accessibility Patterns

UIKit无障碍开发模式

When working with UIKit views:
  • Set
    isAccessibilityElement = true
    on meaningful custom views.
  • Set
    accessibilityLabel
    on all interactive elements without visible text.
  • Use
    .insert()
    and
    .remove()
    for trait modification (not direct assignment).
  • Set
    accessibilityViewIsModal = true
    on custom overlay views to trap focus.
  • Post
    .announcement
    for transient status messages.
  • Post
    .layoutChanged
    with a target view for partial screen updates.
  • Post
    .screenChanged
    for full screen transitions.
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 = true

Accessibility Custom Content

无障碍自定义内容

Use
accessibilityCustomContent
for supplementary details that should not clutter the primary label. VoiceOver users access custom content via the "More Content" rotor:
swift
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
    )
使用
accessibilityCustomContent
添加无需展示在主标签中的补充信息。VoiceOver用户可通过「更多内容」转子访问自定义内容:
swift
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

常见错误

  1. Direct trait assignment:
    .accessibilityTraits(.isButton)
    overwrites all existing traits. Use
    .accessibilityAddTraits(.isButton)
    .
  2. Missing focus restoration: Dismissing sheets without returning VoiceOver focus to the trigger element.
  3. Ungrouped list rows: Multiple text elements per row create excessive swipe stops. Use
    .accessibilityElement(children: .combine)
    .
  4. Redundant trait in labels:
    .accessibilityLabel("Settings button")
    reads as "Settings button, button." Omit the type.
  5. Missing labels on icon-only buttons: Every
    Image
    -only button MUST have
    .accessibilityLabel
    .
  6. Ignoring Reduce Motion: Always check
    accessibilityReduceMotion
    before movement animations.
  7. Fixed font sizes:
    .font(.system(size: 16))
    ignores Dynamic Type. Use
    .font(.body)
    or similar text styles.
  8. Small tap targets: Icons without
    frame(minWidth: 44, minHeight: 44)
    and
    .contentShape()
    .
  9. Color as sole indicator: Red/green for error/success without text or icon alternatives.
  10. Missing
    .isModal
    on overlays
    : Custom modals without
    .accessibilityAddTraits(.isModal)
    let VoiceOver escape.
  1. 直接赋值特征
    .accessibilityTraits(.isButton)
    会覆盖所有现有特征,应使用
    .accessibilityAddTraits(.isButton)
  2. 缺失焦点恢复:关闭弹窗时未将VoiceOver焦点返还给触发元素。
  3. 列表行未分组:单行包含多个文本元素会导致过多滑动停靠点,应使用
    .accessibilityElement(children: .combine)
  4. 标签包含冗余特征
    .accessibilityLabel("Settings button")
    会播报为「Settings button, button」,应省略元素类型。
  5. 仅含图标的按钮缺失标签:所有仅含
    Image
    的按钮必须添加
    .accessibilityLabel
  6. 忽略减少动态效果设置:添加运动动画前必须检查
    accessibilityReduceMotion
  7. 固定字体大小
    .font(.system(size: 16))
    会忽略动态字体,应使用
    .font(.body)
    等系统文本样式。
  8. 点击区域过小:图标未设置
    frame(minWidth: 44, minHeight: 44)
    .contentShape()
  9. 仅用颜色作为标识:用红绿表示错误/成功时未提供文本或图标替代方案。
  10. 浮层缺失
    .isModal
    特征
    :自定义弹窗未添加
    .accessibilityAddTraits(.isModal)
    会导致VoiceOver焦点逸出。

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 (
    Image(decorative:)
    or
    .accessibilityHidden(true)
    )
  • List rows group content with
    .accessibilityElement(children: .combine)
  • Sheets and dialogs return focus to the trigger on dismiss
  • Custom overlays have
    .isModal
    trait and escape action
  • All tap targets are at least 44x44 points
  • Dynamic Type supported (
    @ScaledMetric
    , system fonts, adaptive layouts)
  • 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
    Sendable
    when passed across concurrency boundaries
所有用户可见视图需验证以下项:
  • 所有可交互元素都配备无障碍标签
  • 自定义控件通过
    .accessibilityAddTraits
    设置了正确特征
  • 装饰性图片已隐藏(
    Image(decorative:)
    .accessibilityHidden(true)
  • 列表行通过
    .accessibilityElement(children: .combine)
    分组内容
  • 底部弹窗和对话框关闭后焦点返还给触发元素
  • 自定义浮层添加了
    .isModal
    特征和退出操作
  • 所有点击区域尺寸至少为44x44点
  • 支持动态字体(
    @ScaledMetric
    、系统字体、自适应布局)
  • 遵循减少动态效果设置(开启时无运动动画)
  • 遵循降低透明度设置(开启时使用纯色背景)
  • 遵循增强对比度设置(使用更深的前景色)
  • 无仅通过颜色传递的信息
  • 滑动展示和上下文菜单功能提供了自定义无障碍操作
  • 仅含图标的按钮配备了标签
  • 分区标题设置了标题特征
  • 跨并发边界传递的自定义无障碍类型和通知载荷符合
    Sendable
    要求

Apple Documentation Reference

Apple文档参考

For the latest API details, use the
apple-docs
MCP server when available:
  • searchAppleDocumentation
    with queries like "accessibility SwiftUI" or "VoiceOver"
  • fetchAppleDocumentation
    with paths like
    /documentation/swiftui/view-accessibility
    or
    /documentation/swiftui/scaledmetric
  • Apple's modifier reference covers labels, values, hints, actions, traits, focus, rotors, and custom content
如需最新API详情,可使用
apple-docs
MCP服务:
  • 调用
    searchAppleDocumentation
    ,传入查询词如「accessibility SwiftUI」或「VoiceOver」
  • 调用
    fetchAppleDocumentation
    ,传入路径如
    /documentation/swiftui/view-accessibility
    /documentation/swiftui/scaledmetric
  • Apple的修饰符参考涵盖了标签、值、提示、操作、特征、焦点、转子和自定义内容等所有能力