· engineering· 约 26 分钟精读
Qubitlab Offline Accessibility Engineering Identifier Semantics — Technical Analysis | KeyRotate Technology
一台不联网、不注册的儿童教学应用,系统字号与旁白这类系统能力到底有没有被接住?本文以密旋科技 QubitLab 的六十二个 Swift 源文件为样本做一次静态普查:一百四十处自动化标识、零处语义修饰符、四百七十五处语义字号与一百四十六处固定点值并存、九处连续动画不受减弱动态效果约束。据此拆开端侧应用里"被看见"与"被读出"这两件常被混为一谈的事,并给出可回归的审计工程线。
一、一台不联网的设备上,“被看见”与”被读出”
设想一个具体场景:家长把平板调成系统最大字号,打开旁白,再把它递给视力偏弱的孩子。孩子要完成的动作是拖动一根滑杆,让反射光线落进高处的靶心。屏幕上的图形照样在动,读数照样在跳,但在最大字号与旁白同时打开之后,界面还能剩下多少可用信息,取决于工程有没有为这两项各留一条通道。
端侧应用把数据不出设备、不注册账户、不采集遥测这些目标做到了之后,界面上剩下的问题其实是一个老问题:内容能不能被感知。这里有三件事经常被当成一件事,实际却是三条独立的通道:被看见,指排版、对比度与可点击区域;被读出,指屏幕阅读器能不能把控件的意图与数值讲清楚;被耐受,指对动态效果的敏感度偏好。三条通道各自对应一项系统设置,也各自对应一段实现代码。
密旋科技的 QubitLab 是一款面向应用商店的移动端量子物理与 STEM 启蒙沙盒应用:九十六关闯关玩法、可交互的布洛赫球面、双比特基础逻辑门拼装与概率测量演示,全程离线完成。本文不评价它的教学效果,只做一次静态普查,样本是该工程六十二个 Swift 源文件、三万两千余行代码。
二、两种”标识”并不是一回事
在移动端工程里,“标识”这个词覆盖了两个内容完全不同的东西。
自动化标识是一个地址:它让自动化走查在不依赖文案、语言与坐标的前提下找到某个元素,因此必须唯一、稳定、与外观解耦。无障碍语义是另一回事:它是屏幕阅读器朗读时的全部信息来源,包含控件的名称、当前数值、是否需要特别提示、以及若干元素要不要合并成一个整体来朗读。
这两件事在 2023 年之后被系统明确地区分开来:Apple 在介绍自动化可访问性审计的同时引入了自动化元素的概念,用来把某个元素暴露给自动化,同时不改变它的无障碍语义。这条设计含义很清楚——可以为了走查而暴露元素,但不能顺手把语义一起改掉,反过来也一样。
把 QubitLab 的源码按修饰符逐个统计,得到的分布如下。
| 修饰符 | 出现次数 | 面向对象 |
|---|---|---|
| accessibilityIdentifier | 140 | 自动化能否定位元素 |
| accessibilityLabel | 0 | 旁白读出的名称 |
| accessibilityValue | 0 | 旁白读出的当前值 |
| accessibilityHint | 0 | 操作后会发生什么 |
| accessibilityHidden | 0 | 装饰元素是否需要隐藏 |
| accessibilityElement | 0 | 多个子元素是否合并朗读 |
| accessibilitySortPriority | 0 | 朗读顺序 |
一百四十处注册对应一百二十二个不同的名称,分布在二十三个视图文件里。工程在自动化这一侧做得相当细:界面上的主要控件几乎都有名字。而在这六十二个文件里,用于旁白的语义修饰符一处也没有。工程里出现的全部一百四十处无障碍相关修饰符,全部是标识。
三、标识的第三种用法:把名字当槽位
继续追下去,会看到一种更细的用法。在激光与反光镜这一关的视图里,同一个名称——缝间距滑杆——被注册了四次,分别挂在四根滑杆上:反光镜角度(取值 15 到 75,步长 1)、棱镜角度、水滴间距(取值 16 到 56,步长 2),以及真正的缝间距。四根滑杆控制的是四个不同的物理量,分属同一关的不同阶段,但共用一个名字。
这不是笔误,而是一种刻意的节省:一关的多个阶段在任一时刻只会渲染其中一个,所以同屏只会出现一根同名滑杆,走查脚本用这一个名字就足以覆盖全部阶段。工程里的走查代码也确实是这样写的——它按滑杆类型加这个名字去取元素,再把归一化位置调到某个值,然后点验证按钮。
代价是这个名字不再描述被控对象:只看名字,说不出它控制的是角度还是间距;取值范围、步长、单位这三项信息都不在名字里,而在被挂载的那根滑杆的代码里。这种命名方式把标识从”元素地址”变成了”槽位名”:它描述的是”这一屏里那根连续量输入控件”,而不是”反光镜角度”。
只要一屏之内同类槽位不超过一个,这种做法就是可靠的;一旦某个阶段同时渲染两根同类滑杆,按名字查询就会从唯一匹配变成多匹配,走查脚本必须改写。这是这套命名法隐含的前提条件,而它并没有写在代码里。
四、语义空缺的实际后果
零处语义修饰符会带来三种后果,值得分开评估,因为它们的严重程度并不相同。
| 现象 | 直接后果 | 实际严重程度 |
|---|---|---|
| 微调按钮只有系统图标 | 旁白读不出”增大”或”减小” | 中:相邻有数值读数可推断 |
| 滑杆数值没有量纲 | 读出 45,读不出 45 度 | 中:读数区通常带单位文字 |
| 卡片内多行文字未合并 | 旁白逐行朗读,句读被打断 | 低:不影响操作完成 |
第一项最容易验证:加号与减号按钮的标签是系统图标,不带文字。系统对图标按钮有一定程度的名称推导,但推导出来的名称是图标的形状语义,不是”把角度加一度”。孩子听到的是形状名称,要靠旁边的数值变化去猜这个按钮做了什么。第二项是滑杆的共性问题:滑杆自身会朗读当前数值,但数值只有数字,没有单位。取值范围 15 到 75 与步长 1 这两项信息只存在于代码里,界面上靠旁边的读数文字补齐。
这些并不是把应用判成不可用。界面在这些控件附近普遍配有数值读数与说明文字,读数把缺掉的上下文补回来了一部分,所以实际风险被摊薄。严格地说,这条路径是”读出来再推断”,而不是”直接读出来”。对面向低龄学习者的产品,这两种体验的差别更明显:成人能靠常识补的缺口,孩子补不了。
五、字号缩放的两条轨道
排版通道的问题更结构化。工程里同时存在两套字号写法:一套是语义文本样式,例如大标题、正文、小注这类带语义层级的样式;另一套是直接写死的点值,例如字号 13 加粗、字号 9、字号 8。
两者的差别在于是否参与系统字号缩放。语义样式会按系统的缩放因子整体放大,固定点值不会。记系统缩放因子为 ,语义样式的基准点值为 ,固定点值为 ,则两类文字的实际尺寸为
两者之比
随缩放因子增大而单调减小。当系统字号调到较大档位时,这个比值会降到 1 以下,也就是固定点值的辅助文字反而比语义样式的正文更小——原本只是为了紧凑而选的小角标,在最大字号下变成了屏上最小的字,而系统认为用户需要的是更大的字。这不是布局变乱,而是层级关系被拉开。
两套写法的分布如下。语义样式共 475 处,以 caption(136 处)、caption2(111 处)、subheadline(91 处)、headline(80 处)为主,另有 title3、title2、callout、footnote、title、largeTitle 若干。固定点值共 146 处,其中 144 处是字面常量,2 处以计算属性传入。
| 点值 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 26 至 56 |
|---|---|---|---|---|---|---|---|---|---|
| 处数 | 4 | 12 | 28 | 8 | 25 | 22 | 28 | 2 | 14 |
| 是否随系统放大 | 否 | 否 | 否 | 否 | 否 | 否 | 否 | 否 | 否 |
按点值分档看: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 点 | 不满足 |
用式子写出来,设按钮的视觉尺寸为宽 、高 :
32 点到 38 点的方形按钮跨过了网页内容可访问性准则的最低线,但没有达到平台自己的建议值。这不是”不合格”,而是两个口径的比较对象不同:前者是面向广泛媒介形式的最低门槛,后者是针对手持触摸场景的经验值。工程里另外还有一处细节:这些按钮没有设置命中形状,也就是说命中区域恰好等于视觉框,没有被向外扩张过。对低龄用户的手指而言,多出来的那 6 到 12 点往往是误触与命中之间的差别。
对比度是排版通道的另一条线。工程在应用入口处一次性声明了深色外观,因此整个界面只在一个色域内呈现。对比度按相对亮度比计算:
单一色域带来的是一致性:所有颜色组合只需在深色背景下验证一次。代价是这条线只在一半的色域内被验证过,而用户在系统层面的浅色偏好被应用单方面覆盖了。
七、画面不是语义:直绘图元与旁路文本
对这类应用而言,最需要被读出的部分恰恰最不容易被读出。布洛赫球面、双缝图样、势垒穿透这几处核心视觉,以及若干小游戏的画面,都是按帧直绘的图形——它们不是由可枚举的界面元素拼起来的,而是一整块自绘的画布。
对屏幕阅读器来说,这块画布是一个整体容器。画布内部发生了什么——干涉条纹移动了、波包透过了势垒、靶心被击中——旁白都读不到,因为它读不到”像素”。工程里有九处按动画时间线刷新的绘制容器,覆盖七个视图文件;其中大部分位于关卡与小游戏的模拟画面上。
于是可达性只能走旁路:把画布上真正要传达的量,用文本读数再讲一遍。工程里确实有这样一层,八枚读数是它的骨架——势垒能量、势垒高度、势垒厚度、弗兰克-赫兹电流、放射时钟的关卡阶段与历史读数等,都是独立的、带标识的文本读数区。除此之外,还有一套交互反馈提示卡,在操作发生时把目标值与进度以文字形式呈现出来。
这决定了可达性的上限:读数层对可交互物理量的覆盖率。如果每个能被用户改变的物理量都配了一处带单位的读数,那么旁白用户虽然看不到画面,仍能完成整个实验;反之,画面越漂亮,读不到的信息越多。判断一个自绘实验界面是否可访问,与其看它的图形有多精致,不如数它的读数覆盖了多少个变量。
八、动效与色觉:单点声明的得与失
第三项系统设置是减弱动态效果。工程里有二十三处显式动画调用、九处按时间线刷新的连续动画、一处无限循环,但没有任何一处读取系统的减弱动态效果偏好。也就是说,无论用户在系统设置里有没有打开这一项,画面里的连续动画都照常播放。
这一项在前述准则里属于最高一致性级别的要求,并非硬性门槛;它要照顾的是对运动敏感的用户与被持续动画分散注意力的用户。对一个面向低龄学习者的产品,后一种情况更常见:动画在这类产品里通常承担”吸引注意”的职能,但同一个动画对另一个孩子就是持续的干扰源。
色觉方面同样是单点声明的形状:工程没有读取系统”无需依赖颜色区分内容”这一偏好,也没有读取降低透明度偏好。前面提到的读数、徽章、状态标签都同时用文字承载信息,所以颜色不是唯一通道,这一点是成立的;但把”用文字兜底”作为设计策略,与”尊重用户的色觉偏好设置”是两件事,前者已做到,后者没有接。
九、把静态结论变成门禁
上面这些结论都来自静态普查,规模化的价值在于它们可以被转换成可回归的断言。从系统 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/