WWDC Quick Look 💓 By SwiftGGTeam
Refine accessibility for custom controls

Refine accessibility for custom controls

观看原视频

Highlight

Apple 为自定义控件提供了完整的无障碍 API,包括可调整特性、穿透手势、自定义操作和直接触摸,让依赖辅助技术的用户也能完全使用复杂交互控件。

核心内容

做一个自定义 UI 控件不难,难的是让它对所有用户都可用。

想象一下,你做了一个炫酷的咖啡调节控件——手指上下滑动就能调整咖啡量。视力正常的用户看一眼就懂:填充高度代表咖啡量,手势就是拖拽。大脑在零点几秒内处理完形状、位置、预期行为。

但看不见的用户呢?轨道、手柄、位置,这些视觉信息全都不存在。

VoiceOver(苹果内置屏幕阅读器)通过语音传递这些信息。标准的 Slider 控件会读出:“亮度,50%,可调节。上下滑动调整数值。” 用户知道这是什么、当前值是多少、该怎么操作。

这就是自定义控件无障碍的四个指导原则:

  1. 目的清晰——控件是做什么的
  2. 值可见——如果有值,要能被获取
  3. 操作明确——用户能做什么动作
  4. 反馈及时——操作后有什么变化

很多自定义控件只考虑了视觉交互,忽略了辅助技术。结果就是依赖 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 暴露当前值
  • .adjustable trait 表示这个控件可以调整
  • .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

评论

GitHub Issues · utterances