一次完整的 SwiftUI 无障碍审计:VoiceOver、动态字体与点击区域
无障碍不是发布前打开一次 Inspector 就完成的检查。SwiftUI 能从 Button、TextField、Toggle 等原生控件自动生成大量语义,但自定义布局、图标按钮和动画仍需要开发者说明意图。一次有效审计应同时覆盖语义、阅读顺序、文字缩放、触控区域和动态效果。
先不用眼睛走一遍任务
打开 VoiceOver,从页面入口完成一个核心任务,例如查找图书、加入收藏并返回。每个元素都应回答三个问题:它是什么,它当前有什么值,激活后会发生什么。若读到“heart”或文件名,说明界面把实现细节暴露给了用户。
图标按钮应保留 Button 语义,并提供面向动作的标签:
struct FavoriteButton: View {
@Binding var isFavorite: Bool
var body: some View {
Button {
isFavorite.toggle()
} label: {
Image(systemName: isFavorite ? "heart.fill" : "heart")
.frame(minWidth: 44, minHeight: 44)
.contentShape(Rectangle())
}
.accessibilityLabel(isFavorite ? "Remove from favorites" : "Add to favorites")
.accessibilityValue(isFavorite ? "Favorite" : "Not favorite")
}
}
标签描述动作,值描述当前状态。不要再添加重复 hint,例如“double tap to activate”,因为 VoiceOver 已经会说明按钮的标准交互。
检查阅读顺序与分组
视觉上的 HStack 不一定形成合理语音顺序。标题、作者和状态如果共同描述一本书,可以用 .accessibilityElement(children: .combine) 合并;可独立操作的按钮则不要一起合并。只有当布局顺序无法调整时再使用 accessibilitySortPriority,否则代码中的优先级容易与后续界面重排失去同步。
装饰图像应使用 Image(decorative:) 或 .accessibilityHidden(true)。但不要仅因为图片旁边有文字就隐藏它:如果图片承载文字没有表达的信息,它仍需要标签。
把字体放大到极限
在最大辅助字体下检查每个页面。固定高度、单行限制和紧凑横向布局是常见问题。允许文字换行,用 ViewThatFits 或尺寸类别切换成纵向布局,避免用 minimumScaleFactor 把重要文本缩回难以阅读的大小。
动态字体检查不只看“是否截断”,还要看操作是否仍在合理顺序、错误信息是否靠近输入、滚动后焦点是否可找到。自定义字体应通过相对文本样式缩放,而不是固定点数。
点击区域与视觉边界分开
小图标可以保持视觉尺寸,但可交互区域应足够大。示例中的最小 frame 和 contentShape 让透明区域也参与命中测试。相邻按钮之间要留出间距,避免运动控制不精确的用户误触。
颜色不能是唯一信息。错误状态同时使用文字或图标;选择状态同时更新无障碍值。打开“降低动态效果”后,连续缩放、视差和大范围转场应切换为淡入淡出或无动画。还要测试“增强对比度”和“降低透明度”,确认材料背景上的文字仍可辨认。
把审计变成回归流程
为关键页面保留一份短清单:VoiceOver 核心流程、最大字体、横竖屏、浅色/深色、高对比度、减少动态效果,以及键盘或 Switch Control 的焦点。自动检查可以发现缺少标签和部分对比度问题,但无法判断播报是否自然、任务是否可完成。
真正的验收标准不是“控件都有 accessibilityLabel”,而是用户能理解状态、完成任务并从错误中恢复。优先使用语义正确的原生控件,再用少量 modifier 补充产品特有信息,SwiftUI 的无障碍实现会更稳定,也更容易随界面演进。