ios-accessibility by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill ios-accessibility每个面向用户的视图都必须能够与 VoiceOver、切换控制、语音控制、全键盘访问和其他辅助技术配合使用。本技能涵盖了构建无障碍 iOS 应用所需的模式和 API。
.accessibilityLabel。.accessibilityAddTraits 拥有正确的特征(切勿直接赋值)。@ScaledMetric、自适应布局)。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
VoiceOver 以固定的、不可配置的顺序读取元素属性:
标签 -> 值 -> 特征 -> 提示
设计标签、值和提示时,请牢记此阅读顺序。
有关详细的 SwiftUI 修饰符示例(标签、提示、特征、分组、自定义控件、可调节操作和自定义操作),请参阅 references/a11y-patterns.md。
焦点管理是大多数应用失败的地方。当表单、警告框或弹出窗口关闭时,VoiceOver 焦点必须返回到触发它的元素。
@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 上下文中强制宣布更改或移动焦点时:
// 宣布状态变化(例如,"项目已删除"、"上传完成")
UIAccessibility.post(notification: .announcement, argument: "上传完成")
// 部分屏幕更新 -- 将焦点移动到特定元素
UIAccessibility.post(notification: .layoutChanged, argument: targetView)
// 全屏过渡 -- 将焦点移动到新屏幕
UIAccessibility.post(notification: .screenChanged, argument: newScreenView)
有关动态类型和自适应布局示例,包括 @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("设置", systemImage: "gear")
// 纯图标按钮:**必须**有一个无障碍标签
Button(action: { }) {
Image(systemName: "gear")
}
.accessibilityLabel("设置")
辅助访问为认知障碍用户提供了简化的界面。应用应支持此模式:
// 检查辅助访问是否启用 (iOS 18+)
@Environment(\.accessibilityAssistiveAccessEnabled) var isAssistiveAccessEnabled
var body: some View {
if isAssistiveAccessEnabled {
SimplifiedContentView()
} else {
FullContentView()
}
}
关键指南:
使用 UIKit 视图时:
在有意义的自定义视图上设置 isAccessibilityElement = true。
在所有没有可见文本的交互元素上设置 accessibilityLabel。
使用 .insert() 和 .remove() 来修改特征(不要直接赋值)。
在自定义覆盖层视图上设置 accessibilityViewIsModal = true 以锁定焦点。
使用 .announcement 发布临时状态消息。
使用目标视图发布 .layoutChanged 以进行部分屏幕更新。
发布 .screenChanged 以进行全屏过渡。
// UIKit 特征修改 customButton.accessibilityTraits.insert(.button) customButton.accessibilityTraits.remove(.staticText)
// 模态覆盖层 overlayView.accessibilityViewIsModal = true
有关 UIKit 无障碍模式和自定义内容示例,请参阅 references/a11y-patterns.md。
ProductRow(product: product)
.accessibilityCustomContent("价格", product.formattedPrice)
.accessibilityCustomContent("评分", "\(product.rating) 分,满分 5 分")
.accessibilityCustomContent(
"库存状态",
product.inStock ? "有货" : "缺货",
importance: .high // .high 表示随元素自动读出
)
.accessibilityTraits(.isButton) 会覆盖所有现有特征。请使用 .accessibilityAddTraits(.isButton)。.accessibilityElement(children: .combine)。.accessibilityLabel("Settings button") 会读作 "Settings button, button。" 请省略类型。Image 的按钮必须有 .accessibilityLabel。accessibilityReduceMotion。.font(.system(size: 16)) 忽略了动态类型。请使用 .font(.body) 或类似的文本样式。frame(minWidth: 44, minHeight: 44) 和 .contentShape()。.isModal:自定义模态视图没有 .accessibilityAddTraits(.isModal) 会让 VoiceOver 焦点逃逸。对于每个面向用户的视图,请验证:
.accessibilityAddTraits 使用了正确的特征Image(decorative:) 或 .accessibilityHidden(true)).accessibilityElement(children: .combine) 对内容进行了分组.isModal 特征和退出操作@ScaledMetric、系统字体、自适应布局)Sendable 的references/a11y-patterns.md每周安装量
417
代码仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex413
gemini-cli410
kimi-cli410
github-copilot410
amp410
cline410
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.
.accessibilityLabel..accessibilityAddTraits (never direct assignment).@ScaledMetric, adaptive layouts).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.
See references/a11y-patterns.md for detailed SwiftUI modifier examples (labels, hints, traits, grouping, custom controls, adjustable actions, and custom actions).
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.
@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.
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
}
}
}
}
}
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
}
}
}
Custom overlay views need the .isModal trait to trap VoiceOver focus and an escape action for dismissal:
CustomDialog()
.accessibilityAddTraits(.isModal)
.accessibilityAction(.escape) { dismiss() }
When you need to announce changes or move focus imperatively in UIKit contexts:
// 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)
See references/a11y-patterns.md for Dynamic Type and adaptive layout examples, including @ScaledMetric and minimum tap target patterns.
Rotors let VoiceOver users quickly navigate to specific content types. Add custom rotors for content-heavy screens. See references/a11y-patterns.md for complete rotor examples.
Always respect these environment values:
@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var contrast // .standard or .increased
@Environment(\.legibilityWeight) var legibilityWeight // .regular or .bold
Replace movement-based animations with crossfades or no animation:
withAnimation(reduceMotion ? nil : .spring()) {
showContent.toggle()
}
content.transition(reduceMotion ? .opacity : .slide)
// 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)
// 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")
Assistive Access provides a simplified interface for users with cognitive disabilities. Apps should support this mode:
// Check if Assistive Access is active (iOS 18+)
@Environment(\.accessibilityAssistiveAccessEnabled) var isAssistiveAccessEnabled
var body: some View {
if isAssistiveAccessEnabled {
SimplifiedContentView()
} else {
FullContentView()
}
}
Key guidelines:
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.
See references/a11y-patterns.md for UIKit accessibility patterns and custom content examples.
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
)
.accessibilityTraits(.isButton) overwrites all existing traits. Use .accessibilityAddTraits(.isButton)..accessibilityElement(children: .combine)..accessibilityLabel("Settings button") reads as "Settings button, button." Omit the type.Image-only button MUST have .accessibilityLabel.accessibilityReduceMotion before movement animations.For every user-facing view, verify:
.accessibilityAddTraitsImage(decorative:) or .accessibilityHidden(true)).accessibilityElement(children: .combine).isModal trait and escape action@ScaledMetric, system fonts, adaptive layouts)references/a11y-patterns.mdWeekly Installs
417
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex413
gemini-cli410
kimi-cli410
github-copilot410
amp410
cline410
问题定义框架 | 91位产品负责人方法论,区分问题与解决方案,避免AI技术陷阱
770 周安装
// UIKit trait modification customButton.accessibilityTraits.insert(.button) customButton.accessibilityTraits.remove(.staticText)
// Modal overlay overlayView.accessibilityViewIsModal = true
.font(.system(size: 16)) ignores Dynamic Type. Use .font(.body) or similar text styles.frame(minWidth: 44, minHeight: 44) and .contentShape()..isModal on overlays: Custom modals without .accessibilityAddTraits(.isModal) let VoiceOver escape.Sendable