· engineering· 约 26 分钟精读

Qubitlab Offline Accessibility Engineering Identifier Semantics — Technical Analysis | KeyRotate Technology

核心要点速览 · TL;DR 概要

一台不联网、不注册的儿童教学应用,系统字号与旁白这类系统能力到底有没有被接住?本文以密旋科技 QubitLab 的六十二个 Swift 源文件为样本做一次静态普查:一百四十处自动化标识、零处语义修饰符、四百七十五处语义字号与一百四十六处固定点值并存、九处连续动画不受减弱动态效果约束。据此拆开端侧应用里"被看见"与"被读出"这两件常被混为一谈的事,并给出可回归的审计工程线。

Qubitlab Offline Accessibility Engineering Identifier Semantics — Technical Analysis | KeyRotate Technology

一、一台不联网的设备上,“被看见”与”被读出”

设想一个具体场景:家长把平板调成系统最大字号,打开旁白,再把它递给视力偏弱的孩子。孩子要完成的动作是拖动一根滑杆,让反射光线落进高处的靶心。屏幕上的图形照样在动,读数照样在跳,但在最大字号与旁白同时打开之后,界面还能剩下多少可用信息,取决于工程有没有为这两项各留一条通道。

端侧应用把数据不出设备、不注册账户、不采集遥测这些目标做到了之后,界面上剩下的问题其实是一个老问题:内容能不能被感知。这里有三件事经常被当成一件事,实际却是三条独立的通道:被看见,指排版、对比度与可点击区域;被读出,指屏幕阅读器能不能把控件的意图与数值讲清楚;被耐受,指对动态效果的敏感度偏好。三条通道各自对应一项系统设置,也各自对应一段实现代码。

密旋科技的 QubitLab 是一款面向应用商店的移动端量子物理与 STEM 启蒙沙盒应用:九十六关闯关玩法、可交互的布洛赫球面、双比特基础逻辑门拼装与概率测量演示,全程离线完成。本文不评价它的教学效果,只做一次静态普查,样本是该工程六十二个 Swift 源文件、三万两千余行代码。

二、两种”标识”并不是一回事

在移动端工程里,“标识”这个词覆盖了两个内容完全不同的东西。

自动化标识是一个地址:它让自动化走查在不依赖文案、语言与坐标的前提下找到某个元素,因此必须唯一、稳定、与外观解耦。无障碍语义是另一回事:它是屏幕阅读器朗读时的全部信息来源,包含控件的名称、当前数值、是否需要特别提示、以及若干元素要不要合并成一个整体来朗读。

这两件事在 2023 年之后被系统明确地区分开来:Apple 在介绍自动化可访问性审计的同时引入了自动化元素的概念,用来把某个元素暴露给自动化,同时不改变它的无障碍语义。这条设计含义很清楚——可以为了走查而暴露元素,但不能顺手把语义一起改掉,反过来也一样。

把 QubitLab 的源码按修饰符逐个统计,得到的分布如下。

修饰符出现次数面向对象
accessibilityIdentifier140自动化能否定位元素
accessibilityLabel0旁白读出的名称
accessibilityValue0旁白读出的当前值
accessibilityHint0操作后会发生什么
accessibilityHidden0装饰元素是否需要隐藏
accessibilityElement0多个子元素是否合并朗读
accessibilitySortPriority0朗读顺序

一百四十处注册对应一百二十二个不同的名称,分布在二十三个视图文件里。工程在自动化这一侧做得相当细:界面上的主要控件几乎都有名字。而在这六十二个文件里,用于旁白的语义修饰符一处也没有。工程里出现的全部一百四十处无障碍相关修饰符,全部是标识。

同一控件的两种描述:自动化标识轨与无障碍语义轨滑杆 · 连续量角度或间距的连续取值微调按钮 · 加减一只有系统图标,没有文字自动化标识轨140 处注册,122 个名称,覆盖 23 个视图文件无障碍语义轨标签与数值与提示三项全工程均为 0 处同名标识复用:缝间距滑杆这一名称出现四次分别挂在反光镜角度、棱镜角度、水滴间距与缝间距上标识回答的是自动化能否找到控件,语义回答的是旁白能否说清控件两条轨道互不替代,注册了前者不等于完成了后者

三、标识的第三种用法:把名字当槽位

继续追下去,会看到一种更细的用法。在激光与反光镜这一关的视图里,同一个名称——缝间距滑杆——被注册了四次,分别挂在四根滑杆上:反光镜角度(取值 15 到 75,步长 1)、棱镜角度、水滴间距(取值 16 到 56,步长 2),以及真正的缝间距。四根滑杆控制的是四个不同的物理量,分属同一关的不同阶段,但共用一个名字。

这不是笔误,而是一种刻意的节省:一关的多个阶段在任一时刻只会渲染其中一个,所以同屏只会出现一根同名滑杆,走查脚本用这一个名字就足以覆盖全部阶段。工程里的走查代码也确实是这样写的——它按滑杆类型加这个名字去取元素,再把归一化位置调到某个值,然后点验证按钮。

代价是这个名字不再描述被控对象:只看名字,说不出它控制的是角度还是间距;取值范围、步长、单位这三项信息都不在名字里,而在被挂载的那根滑杆的代码里。这种命名方式把标识从”元素地址”变成了”槽位名”:它描述的是”这一屏里那根连续量输入控件”,而不是”反光镜角度”。

只要一屏之内同类槽位不超过一个,这种做法就是可靠的;一旦某个阶段同时渲染两根同类滑杆,按名字查询就会从唯一匹配变成多匹配,走查脚本必须改写。这是这套命名法隐含的前提条件,而它并没有写在代码里。

四、语义空缺的实际后果

零处语义修饰符会带来三种后果,值得分开评估,因为它们的严重程度并不相同。

现象直接后果实际严重程度
微调按钮只有系统图标旁白读不出”增大”或”减小”中:相邻有数值读数可推断
滑杆数值没有量纲读出 45,读不出 45 度中:读数区通常带单位文字
卡片内多行文字未合并旁白逐行朗读,句读被打断低:不影响操作完成

第一项最容易验证:加号与减号按钮的标签是系统图标,不带文字。系统对图标按钮有一定程度的名称推导,但推导出来的名称是图标的形状语义,不是”把角度加一度”。孩子听到的是形状名称,要靠旁边的数值变化去猜这个按钮做了什么。第二项是滑杆的共性问题:滑杆自身会朗读当前数值,但数值只有数字,没有单位。取值范围 15 到 75 与步长 1 这两项信息只存在于代码里,界面上靠旁边的读数文字补齐。

这些并不是把应用判成不可用。界面在这些控件附近普遍配有数值读数与说明文字,读数把缺掉的上下文补回来了一部分,所以实际风险被摊薄。严格地说,这条路径是”读出来再推断”,而不是”直接读出来”。对面向低龄学习者的产品,这两种体验的差别更明显:成人能靠常识补的缺口,孩子补不了。

五、字号缩放的两条轨道

排版通道的问题更结构化。工程里同时存在两套字号写法:一套是语义文本样式,例如大标题、正文、小注这类带语义层级的样式;另一套是直接写死的点值,例如字号 13 加粗、字号 9、字号 8。

两者的差别在于是否参与系统字号缩放。语义样式会按系统的缩放因子整体放大,固定点值不会。记系统缩放因子为 s(t)s(t),语义样式的基准点值为 f0f_0,固定点值为 pp,则两类文字的实际尺寸为

fsemantic=f0 s(t),ffixed=pf_{\mathrm{semantic}} = f_0 \, s(t), \qquad f_{\mathrm{fixed}} = p

两者之比

r(t)=pf0 s(t)r(t) = \frac{p}{f_0 \, s(t)}

随缩放因子增大而单调减小。当系统字号调到较大档位时,这个比值会降到 1 以下,也就是固定点值的辅助文字反而比语义样式的正文更小——原本只是为了紧凑而选的小角标,在最大字号下变成了屏上最小的字,而系统认为用户需要的是更大的字。这不是布局变乱,而是层级关系被拉开。

两套写法的分布如下。语义样式共 475 处,以 caption(136 处)、caption2(111 处)、subheadline(91 处)、headline(80 处)为主,另有 title3、title2、callout、footnote、title、largeTitle 若干。固定点值共 146 处,其中 144 处是字面常量,2 处以计算属性传入。

点值789101112131426 至 56
处数412288252228214
是否随系统放大否否否否否否否否否

按点值分档看:11 点及以下共 77 处,12 到 14 点共 52 处,26 点以上的展示级共 14 处。11 点以下这一档值得单独记一笔——它们大多挂在读数标签、角标与状态小字上,放大后依然不放大。

再往下一层,是截断与高度的问题。工程里显式限制行数的写法只有 5 处,允许缩放文字以适配宽度的写法一处也没有。这意味着放大字号之后,文本主要靠自动换行来消化增量,容器高度需要随内容自适应;而前面提到的微调按钮是固定尺寸的方形按钮,共 27 处,宽高在 32 到 38 点之间,不随字号变化。也就是说,文本能长高,按钮不能长高,放大到极限时两者会争夺同一行的空间。

六、命中区域:两个口径,两套结论

可点击区域这一项,在标准文本里有两条不同的线,而且很容易被混为一谈。

口径要求本例 32 至 38 点方形按钮
WCAG 2.2 成功准则 2.5.8(AA)指针目标不小于 24 × 24 像素满足
平台设计规范对按钮的建议命中区域不小于 44 × 44 点不满足

用式子写出来,设按钮的视觉尺寸为宽 ww、高 hh:

hit=w×h,min⁡(w,h)≥24  (AA),min⁡(w,h)≥44  (平台建议)\mathrm{hit} = w \times h, \qquad \min(w, h) \ge 24 \; (\mathrm{AA}), \qquad \min(w, h) \ge 44 \; (\mathrm{平台建议})

32 点到 38 点的方形按钮跨过了网页内容可访问性准则的最低线,但没有达到平台自己的建议值。这不是”不合格”,而是两个口径的比较对象不同:前者是面向广泛媒介形式的最低门槛,后者是针对手持触摸场景的经验值。工程里另外还有一处细节:这些按钮没有设置命中形状,也就是说命中区域恰好等于视觉框,没有被向外扩张过。对低龄用户的手指而言,多出来的那 6 到 12 点往往是误触与命中之间的差别。

对比度是排版通道的另一条线。工程在应用入口处一次性声明了深色外观,因此整个界面只在一个色域内呈现。对比度按相对亮度比计算:

CR=L1+0.05L2+0.05≥4.5CR = \frac{L_1 + 0.05}{L_2 + 0.05} \ge 4.5

单一色域带来的是一致性:所有颜色组合只需在深色背景下验证一次。代价是这条线只在一半的色域内被验证过,而用户在系统层面的浅色偏好被应用单方面覆盖了。

七、画面不是语义:直绘图元与旁路文本

对这类应用而言,最需要被读出的部分恰恰最不容易被读出。布洛赫球面、双缝图样、势垒穿透这几处核心视觉,以及若干小游戏的画面,都是按帧直绘的图形——它们不是由可枚举的界面元素拼起来的,而是一整块自绘的画布。

对屏幕阅读器来说,这块画布是一个整体容器。画布内部发生了什么——干涉条纹移动了、波包透过了势垒、靶心被击中——旁白都读不到,因为它读不到”像素”。工程里有九处按动画时间线刷新的绘制容器,覆盖七个视图文件;其中大部分位于关卡与小游戏的模拟画面上。

可访问性通道:三项系统设置贯穿三层实现系统字号缩放旁白语义朗读减弱动态效果语义层:旁白能读到什么是否给出标签、数值、提示与元素合并全工程 0 处排版层:字号是否随系统放大语义样式与固定点值两条轨道并行475 处对 146 处命中层:可点击区域与形状方形微调按钮与命中形状的覆盖情况27 处按钮,0 处形状像素画布面布洛赫球与双缝图样为直绘图元旁白只能读到一个整体容器九处连续动画没有减弱分支文本读数面八枚读数标识承载数值旁路交互反馈提示卡是第二条通道覆盖每个可交互量才算完整三项设置各自对应一条实现通道,任何一条缺失都会在界面上留下痕迹

于是可达性只能走旁路:把画布上真正要传达的量,用文本读数再讲一遍。工程里确实有这样一层,八枚读数是它的骨架——势垒能量、势垒高度、势垒厚度、弗兰克-赫兹电流、放射时钟的关卡阶段与历史读数等,都是独立的、带标识的文本读数区。除此之外,还有一套交互反馈提示卡,在操作发生时把目标值与进度以文字形式呈现出来。

这决定了可达性的上限:读数层对可交互物理量的覆盖率。如果每个能被用户改变的物理量都配了一处带单位的读数,那么旁白用户虽然看不到画面,仍能完成整个实验;反之,画面越漂亮,读不到的信息越多。判断一个自绘实验界面是否可访问,与其看它的图形有多精致,不如数它的读数覆盖了多少个变量。

八、动效与色觉:单点声明的得与失

第三项系统设置是减弱动态效果。工程里有二十三处显式动画调用、九处按时间线刷新的连续动画、一处无限循环,但没有任何一处读取系统的减弱动态效果偏好。也就是说,无论用户在系统设置里有没有打开这一项,画面里的连续动画都照常播放。

这一项在前述准则里属于最高一致性级别的要求,并非硬性门槛;它要照顾的是对运动敏感的用户与被持续动画分散注意力的用户。对一个面向低龄学习者的产品,后一种情况更常见:动画在这类产品里通常承担”吸引注意”的职能,但同一个动画对另一个孩子就是持续的干扰源。

色觉方面同样是单点声明的形状:工程没有读取系统”无需依赖颜色区分内容”这一偏好,也没有读取降低透明度偏好。前面提到的读数、徽章、状态标签都同时用文字承载信息,所以颜色不是唯一通道,这一点是成立的;但把”用文字兜底”作为设计策略,与”尊重用户的色觉偏好设置”是两件事,前者已做到,后者没有接。

九、把静态结论变成门禁

上面这些结论都来自静态普查,规模化的价值在于它们可以被转换成可回归的断言。从系统 17 版本起,自动化测试框架提供了 performAccessibilityAudit 方法,它对当前界面直接做无障碍审计,不需要写任何断言——发现问题,用例直接失败。

审计可以分成若干类型,覆盖的项目包括对比度、可点击区域、动态字体、文本截断、元素描述的充分性、元素检测、父子关系、可执行动作与特征标注。把这九类当作一份清单,与本文前面的六节可以逐项对上:语义空缺对应元素描述充分性,字号双轨对应动态字体,固定尺寸按钮对应可点击区域,行数限制对应文本截断,单一色域对应对比度。

使用时有三点需要注意。第一,审计只覆盖当前屏幕,也就是此刻渲染出来的元素,因此完整的覆盖需要在每个界面各自跑一次,测试计划是把这类用例分组管理的地方。第二,审计适合挂在用例的收尾阶段统一执行,让每个走到该界面的用例顺便审一遍。第三,审计结果允许被过滤:拿到问题元素与问题类型之后,可以按元素或按类型决定忽略哪些条目,这是处理误报的唯一正常途径,而不是全局关掉某一类审计。

这样做有一个明确的边界:审计能发现”没给描述”,不能评价”描述是否可理解”。一个把按钮命名为”按钮一”的实现同样能通过元素描述充分性检查,却对旁白用户毫无帮助。审计解决的是覆盖率问题,不是质量问题。

十、这份普查没有覆盖什么

回到标题的限定语。以上结论都来自静态普查,说明工程里有什么、缺什么,不说明用户实际体验如何。把这些结论当作用户测试的替代品是错的——真正有效的验证,仍然是在设备上打开旁白,把整条主流程走一遍,再在最大字号下走一遍。

第二条边界是标识不等于语义。这一点前面已经展开,但值得再强调一次:标识服务于自动化,且在被复用为槽位名之后连唯一性都不再保证;把它当作无障碍工作的完成凭据,是这类工程里常见的一处误判。

第三条边界是固定点值不等于不合格。字号是否必须随系统缩放,取决于它承载的是什么信息:读数与说明文字承载信息,应当随缩放;而纯装饰性的刻度标记与背景纹样,写死点值并无问题。普查给出的是分档,不是判决。

第四条边界是最容易忽略的:审计通过不等于可用,字号放大不等于可读,命中区域达标不等于不会误触。代码只能为其中一部分提供证据。

十一、结语:四条可以落地的工程线

收成四条可执行的线:

第一,语义层补齐。为每一类可交互控件给出名称与量纲:滑杆要说得出它控制什么、单位是什么;微调按钮要有文字标签,而不是只留图标。这一层不改变布局,投入产出比最高。

第二,固定点值分流。承载信息的文字改用语义文本样式,装饰性文字保留点值,并在代码里保留这条区分标准。这样系统字号放大时,需要长大的字会长大,不需要动的元素不会跟着乱。

第三,旁路覆盖率。凡是自绘画面上可被用户改变的物理量,都要有一处带单位的文本读数;这条要求可以直接写成清单核对,而不是靠自觉。

第四,偏好接入与回归门禁。连续动画读取系统的减弱动态效果偏好,并把这个分支写进走查用例;同时把无障碍审计挂进现有走查,让这四项成为每次构建的门禁。

这四条都不涉及新增功能,属于把已有界面补上另一条通道。对端侧离线产品来说,这件事的意义比在联网产品里更直接:离线应用的隐私承诺来自”数据不出设备”,而可访问性承诺来自”信息能进到用户手里”——前者要求少往外发,后者要求多往里说清,两个方向并不冲突。

参考文献

[1] Apple Inc. Performing accessibility audits for your app. https://developer.apple.com/documentation/accessibility/performing-accessibility-audits-for-your-app [2] Apple Inc. XCUIAccessibilityAuditType. https://developer.apple.com/documentation/xcuiautomation/xcuiaccessibilityaudittype [3] Apple Inc. Human Interface Guidelines: Buttons. https://developer.apple.com/design/human-interface-guidelines/buttons [4] Apple Inc. Human Interface Guidelines: Accessibility. https://developer.apple.com/design/human-interface-guidelines/accessibility [5] Apple Inc. WWDC23 Session 10035: Perform accessibility audits for your app. https://developer.apple.com/videos/play/wwdc2023/10035/ [6] W3C. Understanding Success Criterion 2.5.8: Target Size (Minimum). https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html [7] W3C. Understanding Success Criterion 1.4.4: Resize Text. https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html [8] W3C. Understanding Success Criterion 2.3.3: Animation from Interactions. https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html [9] W3C. Understanding Success Criterion 1.4.3: Contrast (Minimum). https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html [10] W3C. Web Content Accessibility Guidelines (WCAG) 2.2. https://www.w3.org/TR/WCAG22/

Share:
Back to Blog