Highlight
Apple 为自定义控件提供了完整的无障碍 API,包括可调整特性、穿透手势、自定义操作和直接触摸,让依赖辅助技术的用户也能完全使用复杂交互控件。
核心内容
做一个自定义 UI 控件不难,难的是让它对所有用户都可用。
想象一下,你做了一个炫酷的咖啡调节控件——手指上下滑动就能调整咖啡量。视力正常的用户看一眼就懂:填充高度代表咖啡量,手势就是拖拽。大脑在零点几秒内处理完形状、位置、预期行为。
但看不见的用户呢?轨道、手柄、位置,这些视觉信息全都不存在。
VoiceOver(苹果内置屏幕阅读器)通过语音传递这些信息。标准的 Slider 控件会读出:“亮度,50%,可调节。上下滑动调整数值。” 用户知道这是什么、当前值是多少、该怎么操作。
这就是自定义控件无障碍的四个指导原则:
- 目的清晰——控件是做什么的
- 值可见——如果有值,要能被获取
- 操作明确——用户能做什么动作
- 反馈及时——操作后有什么变化
很多自定义控件只考虑了视觉交互,忽略了辅助技术。结果就是依赖 VoiceOver、Switch Control 的用户完全用不了。
Apple 提供了一套完整的 API 来解决这个问题。从简单的可调节控件,到复杂的二维均衡器,再到支持多种手势的虚拟宠物,每个场景都有对应的工具。
详细内容
让可调节控件真正可调节
(05:01)
CoffeeDispenserView 是个自定义滑块,上下滑动调整咖啡量。但它没有无障碍支持,VoiceOver 只会读出”按钮”和一些不相关信息。
第一步是把它标记为无障碍元素,并给出清晰的标签和值:
struct CoffeeDispenserView: View {
@State var coffee: Double = 0.0
var body: some View {
CoffeeSlider(value: coffee)
.accessibilityElement()
.accessibilityLabel("Coffee Dispenser")
.accessibilityValue("\(Int(coffee)) ounces")
.accessibilityAddTraits(.adjustable)
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
increaseCoffeeAmount()
case .decrement:
decreaseCoffeeAmount()
}
}
}
}
关键点:
.accessibilityElement()标记这是一个独立的无障碍元素.accessibilityLabel告诉 VoiceOver 这是什么.accessibilityValue暴露当前值.adjustabletrait 表示这个控件可以调整.accessibilityAdjustableAction定义上下滑动分别做什么
这样 VoiceOver 就能读出:“Coffee Dispenser, 6 ounces, adjustable. Swipe up or down with one finger to adjust the value.”
精确控制用穿透手势
(07:05)
标准滑动只能以固定步长调整,比如一次 1 盎司。但有些用户想要 0.5 盎司的精度。
VoiceOver 有个内置能力叫 passthrough gesture(穿透手势):双击并按住,触摸事件会直接发给控件。
struct CoffeeDispenserView: View {
@State var coffee: Double = 0.0
var body: some View {
CoffeeSlider(value: coffee)
.accessibilityActivationPoint(
UnitPoint(x: 0.5, y: 1 - coffee)
)
}
}
关键点:
.accessibilityActivationPoint设置手势起点- x: 0.5 是水平居中,y: 1 - coffee 让起点跟随当前咖啡量
- 这样用户手指无论往哪个方向移动都有调整空间
穿透手势还需要反馈。不能每次值变化都播报,会太吵:
struct CoffeeDispenserView: View {
@State var coffee: Double = 0.0
var body: some View {
CoffeeSlider(value: coffee)
.onChange(of: coffee) { _, newValue in
if sufficientTimeSinceLastAnnouncement() && valueHasChanged() {
cacheLastSpokenValue(newValue)
AccessibilityNotification
.Announcement(newValue)
.post()
}
}
}
}
关键点:
- 检查距上次播报是否超过 0.3 秒
- 检查值是否真的变了
.post()发送无障碍通知
用户双击按住后,手指滑动时会听到:“6.4 ounces… 8.4 ounces… 9.5 ounces”
二维控件用自定义操作
(10:13)
iOS 背景音效里的均衡器是个二维控件,可以同时在频率和振幅两个轴上移动。标准的 .adjustable trait 只支持一个方向的增减,不够用。
这时需要自定义操作(custom actions):
struct EqualizerView: View {
var body: some View {
EqualizerPad()
.accessibilityActions("Move Up") {
increaseY(by: 10)
}
.accessibilityActions("Move Right") {
increaseX(by: 10)
}
.accessibilityActions("Move Down") {
decreaseY(by: 10)
}
.accessibilityActions("Move Left") {
decreaseX(by: 10)
}
}
}
关键点:
.accessibilityActions可以添加多个,每个有名称和闭包- 四个方向分别操作 X 或 Y 轴
- VoiceOver 会读出所有可用操作,用户上下滑动选择、双击执行
用户操作时 VoiceOver 会读:“Move down. Move up. Move right. Frequency 0, amplitude 40.”
多手势控件用直接触摸
(12:47)
虚拟宠物控件支持三种手势:抚摸(呼噜声)、点击(喵叫)、捏合(哈气)。穿透手势不够用,因为用户可能要反复执行多个动作。
直接触摸 API(Direct Touch)是更好的选择:
struct VirtualCat: View {
var cat: CatModel
var body: some View {
InteractiveCatSurface()
.accessibilityLabel("Virtual Cat")
.accessibilityValue(cat.currentReaction.description)
.accessibilityDirectTouch([.requiresActivation])
}
}
关键点:
.accessibilityDirectTouch标记区域支持直接触摸.requiresActivation需要双击才能激活,避免误触- 直接触摸会一直保持激活,直到焦点移到其他元素
- 直接触摸期间,VoiceOver 会语音提示如何操作
还有一个选项 .silentOnTouch,用于控件自己提供音频反馈的场景,避免 VoiceOver 语音和控件音频重叠。
核心启发
为游戏手柄模拟器添加无障碍支持
做什么:用自定义操作支持方向键和 A/B 按键,让 Switch Control 用户也能玩移动游戏。
为什么值得做:很多移动游戏依赖复杂的手势操作,Switch Control 用户完全无法参与。自定义操作把每个游戏动作映射为系统可识别的操作项,扩大用户群体。
怎么开始:给游戏控件添加 .accessibilityActions,为每个方向键和按键定义独立的操作名称和闭包,VoiceOver 用户上下滑动选择、双击执行。
为音乐混音台做二维均衡器无障碍适配
做什么:二维均衡器可以同时在频率和振幅两个轴上移动,用 custom actions 支持频率和振幅的独立调节。
为什么值得做:标准的 .adjustable trait 只支持一个方向的增减,二维控件需要更灵活的操作方式。自定义操作让 VoiceOver 用户也能精确控制每个维度。
怎么开始:给均衡器视图添加多个 .accessibilityActions,分别命名为”上移""下移""左移""右移”,每个操作调整对应的坐标值。
为绘画 App 启用直接触摸模式
做什么:用直接触摸 API 配合 .silentOnTouch,让 VoiceOver 用户直接用手指作画,工具音效不被语音打断。
为什么值得做:绘画是视觉艺术,但视力障碍用户也可以通过触觉和音频反馈创作。直接触摸模式让手指触摸直接作用于画布,.silentOnTouch 避免 VoiceOver 语音和绘画音效重叠。
怎么开始:给画布视图添加 .accessibilityDirectTouch([.requiresActivation, .silentOnTouch]),用 .requiresActivation 防止误触,用 .silentOnTouch 让控件音频独占播放。
为健身强度调节做无障碍适配
做什么:给自定义滑块添加可调节 trait 和穿透手势,支持从粗调到精调。
为什么值得做:健身 App 中的强度调节控件通常依赖视觉反馈(填充高度),看不见的用户无法感知当前值。可调节 trait 暴露当前数值,穿透手势支持精确微调。
怎么开始:添加 .accessibilityElement()、.accessibilityLabel、.accessibilityValue 和 .accessibilityAddTraits(.adjustable),用 .accessibilityAdjustableAction 处理增减,用 .accessibilityActivationPoint 支持穿透手势。
为虚拟宠物互动做直接触摸适配
做什么:虚拟宠物控件支持抚摸、点击、捏合多种手势,用直接触摸模式让 VoiceOver 用户也能完整体验。
为什么值得做:玩具类 App 的乐趣在于多种交互方式,如果只支持单一点击会损失大量体验。直接触摸模式在激活后持续接收触摸事件,支持复杂手势组合。
怎么开始:给交互区域添加 .accessibilityDirectTouch([.requiresActivation]),在激活期间 VoiceOver 会语音提示可用操作,用户可以直接执行各种手势。
关联 Session
- 219 Accessibility Reading — 介绍阅读类内容的无障碍最佳实践
- 221 tvOS Dynamic Type — 文字大小自适应的 tvOS 实现
- 262 SwiftUI — SwiftUI 新特性,包含无障碍相关更新
- 269 SwiftUI — 框架级无障碍支持
- 290 Naming — 命名规范,确保无障碍标签的清晰性
评论
GitHub Issues · utterances