无障碍不是发布前给按钮补几条 accessibilityLabel,也不是让自动审计显示绿色。它是界面结构在不同感知与操作条件下仍能完成任务的能力:文字放大后信息能重排,VoiceOver 能理解关系和动作,Reduce Motion 打开后内容不会依赖空间位移才出现。三者如果分别补丁式处理,最容易得到三个局部“可用”、但完整流程仍然断裂的版本。
本文面向已经使用 SwiftUI 构建实际产品、准备把无障碍纳入组件系统和发布门禁的开发者。前置条件是理解 View、Environment、Layout 与 UI Test。五段精确代码已映射到 examples/swiftui/Sources/WebSunSwiftUIExamples/AccessibilityExamples.swift 与对应测试源,通过 macOS Swift Package 与通用 iOS Simulator 编译;XCUIApplication 审计片段只在 macOS 测试目标中编译,因缺少宿主 App 而未执行。本站也没有为真实 App 执行设备、VoiceOver 与视觉矩阵,所以文章级产品结论仍是 source-reviewed。
版本边界与验收对象
本文以 Xcode 26.6 和稳定 SDK 为基线,讨论的 Text 样式、dynamicTypeSize、VoiceOver 语义 modifier、accessibilityReduceMotion 与 XCUITest accessibility audit 都是稳定路径。Xcode 27 beta 2 只作为后续复核环境,不引入 Beta 专属能力,也不把 Beta Inspector 的结果冒充稳定工具链证据。
Apple 的 Accessibility fundamentals 强调,SwiftUI 会从 View 层级和标准控件生成无障碍信息;WWDC24:Catch up on accessibility in SwiftUI 则展示了如何调整子元素行为、表示方式和动作。工程目标因此不是“让每个 View 都有标签”,而是让辅助技术得到与视觉界面等价、不过度重复的任务模型。
验收也应以任务而非页面为单位。例如“找到一篇文章并加入收藏”至少跨越搜索输入、结果列表、文章标题、收藏动作和状态反馈。每一屏都能被聚焦,不等于这个任务顺序清楚;每个控件都有 label,也不等于结果变化会被用户感知。
第一原则:先保留原生语义
标准 Button、Toggle、TextField、Picker 和 NavigationLink 已经携带角色、状态与交互行为。把它们换成带 onTapGesture 的 HStack,视觉可能相同,却会丢失按钮特征、键盘激活、焦点行为和系统反馈。真正需要自定义外观时,优先给原生控件设置 style,而不是重建控件协议。
语义层级应表达信息关系。一个“标题+作者+日期”的纯展示卡片,逐个朗读可能产生过多停顿,可以合并成一个合理摘要;一个包含独立收藏按钮的卡片,则不应把按钮一起吞进静态文本。children: .combine、.contain 与 .ignore 是结构选择,不是清除审计告警的快捷键。
struct ArticleRow: View {
let article: Article
let toggleSaved: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(article.title).font(.headline)
Text("\(article.author) · \(article.readingTime) 分钟")
.font(.subheadline)
.foregroundStyle(.secondary)
}
.accessibilityElement(children: .combine)
Spacer(minLength: 8)
Button(action: toggleSaved) {
Image(systemName: article.isSaved ? "bookmark.fill" : "bookmark")
}
.accessibilityLabel(article.isSaved ? "取消收藏" : "收藏文章")
}
}
}
图标的可访问名称描述动作或结果,而不是朗读“书签填充图标”。状态与动作也要区分:Toggle 应通过 value 暴露开关状态,Button 的 label 说明按下会发生什么。Hint 只补充无法从名称和上下文推断的信息,避免每个元素都朗读冗长使用说明。
Dynamic Type:放大文字不等于整体缩放
Dynamic Type 的核心是内容适应用户选择的文本尺寸,而不是把设计稿乘以比例。WWDC24:Get started with Dynamic Type 建议使用语义文字样式、让界面响应更大尺寸,并在不同类别下测试。.font(.body)、.headline 等系统样式能随内容尺寸类别调整;固定 font(.system(size: 15)) 不会自动获得相同语义缩放策略。
最常见的失败不是文字不变大,而是容器不允许它变大:固定高度截断两行标题;HStack 里图片和按钮挤占所有宽度;用 minimumScaleFactor 把用户主动放大的字再缩小;把说明文字画进位图。先删除不必要的固定 frame 和 lineLimit(1),允许文本换行,再决定大尺寸下是否重排。
struct ProfileSummary: View {
@Environment(\.dynamicTypeSize) private var typeSize
@ScaledMetric(relativeTo: .body) private var avatarSize: CGFloat = 48
var body: some View {
let layout = typeSize.isAccessibilitySize
? AnyLayout(VStackLayout(alignment: .leading, spacing: 12))
: AnyLayout(HStackLayout(alignment: .firstTextBaseline, spacing: 12))
layout {
Image(systemName: "person.crop.circle.fill")
.resizable()
.scaledToFit()
.frame(width: avatarSize, height: avatarSize)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) {
Text("Sunny").font(.headline)
Text("关注 AI、Flutter、SwiftUI 与 Web Motion")
.font(.body)
}
}
}
}
@ScaledMetric 适合与文字相邻、需要保持触达或视觉比例的非文字尺寸,但不应把所有间距和装饰都同比放大。dynamicTypeSize.isAccessibilitySize 用来改变布局方向,不应该用来隐藏次要功能。Apple 的 dynamicTypeSize 文档 允许读取或限制范围;限制最大尺寸只应出现在确有外部约束的局部组件,并提供其他完整访问路径,不能成为修复布局的默认手段。
测试时至少覆盖默认尺寸、最大的非 accessibility 类别和多个 accessibility 类别,同时检查本地化长文本。观察的不只是截断:阅读顺序是否仍自然,按钮触达面积是否保留,表格是否可以转为纵向字段,弹窗是否可滚动,底部操作是否会遮住内容。大字版往往需要重新编排信息,而不是继续压缩同一行。
VoiceOver:从像素树重建任务树
VoiceOver 用户获得的是线性焦点序列、元素角色、值、动作和必要提示。视觉上的接近不自动等于语义分组,覆盖在一起的图层也可能产生重复朗读。设计时可以把一个流程写成“焦点到哪里、朗读什么、有哪些动作、动作后如何确认”,这比逐个 modifier 检查更接近真实使用。
自绘 Canvas 和图表需要格外谨慎。Canvas 可以把上百条线合并为一个绘制节点,却不会自动生成上百个有意义的数据元素。装饰背景应隐藏;信息图应提供摘要、可聚焦数据项或等价列表;交互图则可以用 accessibilityRepresentation 提供标准控件结构,让视觉渲染与辅助技术表示共享同一份数据。
struct TrendChart: View {
let points: [MetricPoint]
var body: some View {
Canvas { context, size in
drawTrend(points, in: &context, size: size)
}
.accessibilityLabel("最近七天阅读趋势")
.accessibilityRepresentation {
VStack {
ForEach(points) { point in
Text("\(point.day),\(point.value) 次阅读")
}
}
}
}
}
表示层不必复制像素细节,而要保留决策所需信息。如果图表支持选择数据点,表示层也需要可执行的 Button 或可调整动作;仅给整张图一段总结会丢失能力。反过来,纯装饰网格、阴影和重复轴标签不应进入焦点序列。
焦点顺序通常跟 View 结构一致,因此首先修正代码层级,不要大量依赖排序优先级拼出另一棵树。模态内容出现后,背景不应继续被访问;异步错误出现时,要有可聚焦文本或适当通知,而不是只把边框变红。自定义手势必须提供命名动作,拖动或多指手势还应有按钮、Stepper 或 adjustable action 等替代方式。
Reduce Motion:提供另一种表达,不是关闭反馈
用户打开 Reduce Motion,通常是希望减少大范围移动、景深、视差、弹性和持续运动,而不是移除所有状态反馈。Apple 的 accessibilityReduceMotion 环境值 让视图选择替代路径;Accessibility HIG 建议减少三维运动和弹跳,使用淡入淡出等较温和过渡,并避免模糊式转场。
struct ResultPanel: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
let isPresented: Bool
var body: some View {
if isPresented {
ResultContent()
.transition(
reduceMotion
? .opacity
: .asymmetric(
insertion: .move(edge: .bottom).combined(with: .opacity),
removal: .opacity
)
)
}
}
}
重点是两个分支都表达“结果已出现”。完整模式可以用短距离移动建立空间关系;减弱模式用清晰的 opacity、即时布局变化或低幅度强调。无限粒子、自动轮播、鼠标视差、滚动缩放和 shader 扰动应停止或变成静态图,而不是只降低速度——持续更久的运动可能更难受。
也不要在根视图粗暴设置全局 animation(nil)。焦点、进度、展开状态和保存确认仍需要可感知反馈。为 motion token 建立策略更稳妥:进入位移在减弱模式替换为淡入;spring 替换为短 ease;装饰循环停止;用户直接触发、幅度很小且有功能意义的变化再单独评估。
颜色、形状与内容共同编码
无障碍不只覆盖三项环境值。错误不能只用红色表示,选中不能只改变饱和度,图表不能只靠相近色区分系列。系统的 Differentiate Without Color 偏好可以触发图标、下划线、纹理或标签;高对比与浅色、深色外观也要分别检查。颜色增强了信息时,文字、形状或状态值必须同时承担语义。
触控区域与视觉尺寸也不是一回事。小图标可以保留精细外观,但可操作区域应达到平台建议,并避免相邻目标拥挤。横屏、分屏和最大文字下,操作区不得被安全区或键盘挡住。语音控制依赖可见名称与可访问名称的对应关系,“发布”按钮如果只被命名为“提交内容”,用户可能难以用屏幕上看到的词操作。
组件合同要覆盖状态变化
Design System 里的组件不应只保存颜色、字号和圆角,还要声明语义角色、子元素策略、可用动作、大字重排与运动替代。一个异步按钮至少有可操作、进行中、成功和失败四类状态:进行中时避免重复提交但保留可理解名称;成功后用可见文字与适当语义确认;失败时说明原因并提供重试。只旋转图标或震动设备无法覆盖所有用户。
加载骨架属于装饰,不应让 VoiceOver 逐块朗读。真正内容到达后,焦点是否移动取决于任务:用户主动打开详情,焦点应进入新页面的标题或主要内容;后台列表悄悄刷新,不应抢走正在阅读的位置。系统通知也不能滥用,频繁 announcement 会打断当前语音。先让状态文本出现在合理的语义顺序里,只在时间敏感且不会被自然遇到的变化上考虑主动通知。
标题层级、列表和表单分组让用户能快速导航。视觉上放大的字不自动成为 accessibility heading;真正的章节标题应使用语义 trait,同时保持层级一致。重复卡片中的“更多”若没有上下文,会在控件导航里出现十个同名按钮,应命名为“更多:文章标题”或提供等价菜单标签,但屏幕可见名称仍要包含用户能够说出的关键词。
本地化是语义合同的一部分。不要用字符串拼接假定语序,也不要把 VoiceOver label 固定成另一套未同步文案。日期、数值、单位和复数交给本地化格式化器;开发伪本地化与超长翻译可同时暴露 Dynamic Type 之外的宽度问题。右到左语言下,leading/trailing 的运动方向、返回含义和图标也要核对,而不是简单镜像所有动画。
表单错误需要把字段、原因和修复连接起来。提交后只在顶部显示“有错误”,用户仍要逐项猜测;每个无效字段应有可见错误说明,并能通过焦点或摘要跳转抵达。修复后错误消失不能造成焦点突然落到页面开头。权限拒绝、离线和空结果同样是核心状态,必须进入测试 fixture,而不是只验收 happy path。
图 1:同一核心任务需要跨环境与工具验证,自动审计只覆盖其中一列;本站原创 1400×800 程序化 SVG。

图 2(1600 × 900):包容性界面的编辑式视觉隐喻,不代替真实辅助技术测试。OpenAI Image Gen × WEB/SUN,2026-07-16;项目内原创生成资产。
建立从组件到任务的测试矩阵
组件预览适合快速发现截断、焦点元素数量和 motion 分支,但最终证据必须覆盖完整任务。先列出登录以外的核心路径,例如搜索、阅读、收藏和设置;再为每条路径选择默认尺寸、最大文字、VoiceOver、Reduce Motion、不同方向及本地化组合。不需要对所有排列做穷举,但每个风险轴都要在真实任务里出现。
矩阵要标明证据类型。Preview 截图证明的是某个静态布局;UI Test 证明可查询元素和程序化动作;audit 报告覆盖系统可检测规则;设备录屏与手工记录证明实际导航路径;辅助技术用户反馈则发现设计者未预见的认知和操作成本。它们不能相互冒充,尤其不能用一张 audit 通过截图替代任务完成记录。
手工 VoiceOver 测试应在设备上完成:只看屏幕并点元素不能模拟顺序导航。依次使用左右轻扫、按标题或控件导航、执行自定义动作、编辑文本、关闭弹窗并确认焦点返回。Apple 的 accessibility testing 指南 将残障用户参与、手工检查与工具测试视为互补证据。
XCTest 可以在稳定可重现的页面执行系统审计:
@MainActor
func testReadingFlowAccessibility() throws {
let app = XCUIApplication()
app.launchArguments += ["-useFixtureData", "YES"]
app.launch()
app.buttons["打开精选文章"].tap()
try app.performAccessibilityAudit()
}
Apple 的 accessibility audit 文档 说明审计可在 UI Test 中发现一类常见问题。它不会证明阅读顺序合理、文案清楚或任务可完成;动态页面还需要固定 fixture、等待真实状态并审计多个关键屏幕。不要用宽泛 exception handler 永久忽略问题,确有系统误报时应限定类型、元素和原因,并保留复核记录。
常见失败模式
第一类是“标签覆盖”:给容器写一个长 label,同时子按钮仍可聚焦,导致内容重复;或用 .ignore 消除重复时把必要动作一起删除。第二类是“视觉修复”:大字截断后降低 scale,VoiceOver 顺序错误后强设一串 sort priority,Reduce Motion 后把动画时长设成零,却不解决结构。
第三类是“审计替代用户”:自动化全绿便宣布 AA;只在 Simulator 用鼠标点过;只测首页,没有测键盘弹出、加载失败、空状态和权限拒绝。第四类是“替代版缺功能”:图表的无障碍表示只有摘要,无法选择;大字布局隐藏筛选;减弱动态模式关闭轮播后没有手动切换入口。
第五类是“状态无反馈”:收藏成功只变颜色,网络失败只震动,异步加载完成后焦点跳回页面顶部。修复应回到任务:变化由哪些感官通道表达,辅助技术怎样抵达,用户如何确认并继续。
发布门禁
每个公共组件应记录语义角色、组合策略、大字重排、motion 替代和测试标识;每条核心任务应有默认、最大 Dynamic Type、VoiceOver 与 Reduce Motion 证据。发布前用 Accessibility Inspector 和 UI audit 捕获机械问题,再用设备手工完成任务;重要改版邀请真实辅助技术用户参与,记录无法自动化的发现。
最终标准不是“界面在极端设置下长得一样”,而是信息、动作、状态和错误恢复保持等价。保留原生语义,让布局随内容重排,为运动提供明确替代,再让自动化与手工任务互相校验,无障碍才从零散 modifier 变成可以持续维护的系统能力。
Sources
- Accessibility fundamentals — Apple Developer Documentation
- Catch up on accessibility in SwiftUI — WWDC24
- Get started with Dynamic Type — WWDC24
- dynamicTypeSize — Apple Developer Documentation
- accessibilityReduceMotion — Apple Developer Documentation
- Accessibility — Human Interface Guidelines
- Performing accessibility audits for your app — Apple Developer Documentation
- Performing accessibility testing for your app — Apple Developer Documentation