张赐荣,视障者,信息无障碍专家。
深耕Web/PC/移动端可访问性研究与实践工作多年,对跨平台无障碍解决方案拥有深刻的独特理论和丰富的实战经验。
精通视障用户软件交互设计,致力于用专业的能力改善、提升产品可及性体验。

張賜榮

张赐荣的技术博客

博客园 首页 新随笔 联系 订阅 管理

iOS无障碍优化: 如何避免模态弹窗底层焦点穿透

今天和大家聊一个在iOS无障碍开发中特别容易被忽视的问题,如何屏蔽模态弹窗以外的底层焦点。

这个问题说起来简单,但真正踩过坑的人都知道,它背后牵扯到无障碍树、模态语义、焦点管理一连串东西。本文就用一个实际案例,带你深入理解模态弹窗的可访问性焦点管理设计。

一、 VoiceOver是什么

VoiceOver是苹果系统内置的屏幕朗读软件,帮助视障人士操作手机、电脑,Apple 的所有设备都有该功能,包括iPhone、iPad、Mac、Apple TV上都有。视障用户开启它之后,手机的操作方式会完全改变:

  • 单指在屏幕上移动,VoiceOver会朗读手指当前摸到的元素是什么;
  • 单指左右滑动,在可聚焦的元素之间切换焦点;
  • 找到目标后,单指双击屏幕任意位置来激活;
  • 三指上下滑动滚动屏幕,左右滑动翻页;
  • 双指左右扫动执行返回手势等。

对于视障用户来说,屏幕上的一切信息,按钮叫什么、列表有几项、弹窗标题是什么等,都依赖应用是否正确构建和提供无障碍信息。我们开发时有没有给控件设置合适的accessibilityLabel,是否设置了控件的 role, 有没有把该隐藏的元素隐藏掉,有没有把该标记为模态的容器标记上等,直接决定了他们能不能用。

某次,我在一个购物App上做无障碍测试,发现了一个很典型的问题。

二、 典型场景: 购物软件的搜索界面,筛选弹窗打开后,依然能浏览到被遮罩的底层商品元素

那是一个电商App的商品列表页。页面顶部有一排筛选栏,其中有一个“筛选”按钮。点击之后会弹出一个自定义的筛选面板,里面有价格区间、品类选项,底部是“确定”和“取消”。

页面层级大致是这样的:

SearchResultViewController.view
├── FilterBar(顶部筛选栏,含“筛选”按钮)
├── ProductCollectionView(商品列表)
└── FilterPanel(筛选弹窗容器)

打开VoiceOver,单指左右滑动找到“筛选”按钮,双击激活,筛选面板弹出来了。视觉上,面板覆盖了商品列表,背景还加了半透明遮罩。

然后用手指去触摸筛选面板以外的遮罩区域,VoiceOver焦点穿透了模态对话框,居然把商品名称、价格都读了出来。左右滑动,焦点可以在筛选面板里的选项和底层商品列表之间来回跑。

这就是典型的底层焦点问题,也叫焦点穿透。

一句话定义:当一个模态视图覆盖在当前页面之上时,辅助技术仍然能够聚焦并朗读被覆盖的底层元素。

三、为什么视觉覆盖了,VoiceOver还能聚焦浏览?

这里需要理解一个关键区别:视觉遮挡和无障碍遮挡是两回事。

iOS的无障碍浏览基于视图层级和可访问性树结构来枚举可聚焦元素,而不是基于视觉遮挡关系。弹窗盖住了商品列表,但从UIKit的视角看,商品列表的cell仍然是可见的、可聚焦的。VoiceOver无法判断视觉被覆盖住了。

开发者不显式声明模态边界,VoiceOver就不知道哪些内容应该被排除。

那什么是模态弹窗?从交互上说,模态弹窗是一种覆盖在当前页面之上、要求用户先完成或关闭弹窗才能继续操作底层内容的模式。视觉上通常有遮罩、阴影、背景变暗。但无障碍层面的模态需要额外用代码告诉辅助技术:“当前上下文已经切换了,辅助软件应该只关注弹窗内部。”

如果没告知辅助软件,焦点就会跨越到模态外部。

四、 焦点穿透会造成什么问题?

表面上看只是多读了一些不该读的东西,但实际体验上的问题非常严重:

  • 信息结构混乱:用户打开筛选弹窗,左右滑动想切换选项,结果滑到了弹窗背后的商品名称。用户会困惑:我现在到底在弹窗里,还是在商品列表里?
  • 导航迷失:焦点可以在弹窗和底层内容之间自由游走,用户没有任何“我在模态中”的感知。对全盲用户来说,这就像在黑暗中摸到了一个不该存在的房间入口。

在无障碍国标 GB/T 37668-2019《信息技术 互联网内容无障碍可访问性技术要求与测试方法》中,3.3.1.1“功能性组件访问”也明确规定:在页面局部更新后不可见的组件应不可访问;新出现的可见非装饰性组件应能被用户代理正常访问。弹窗打开后,底层商品变成了“不可见的组件”,就应该从无障碍树中隐藏;弹窗本身是“新出现的可见非装饰性组件”,应该能被正常访问。

五、 如何解决?

主要分为两步:划定无障碍模态边界,然后通知辅助技术软件把焦点移进去。

第一步:告诉VoiceOver“这是一个模态对话框”

filterPanel.accessibilityViewIsModal = YES;

accessibilityViewIsModal 是UIKit提供的一个布尔属性。设置为YES后,辅助技术会忽略该视图的所有兄弟视图及其子树,把导航范围只限制在这个视图内部。

这个属性要设在弹窗的根容器视图上,而不是弹窗内部的某个子视图上。Apple官方文档特别强调:“此属性只能在包含模态内容的视图上设置,而不能在模态视图中包含的元素上设置。”。

如果层级实在没法调整,可以补充使用 accessibilityElementsHidden = YES 临时把底层内容从无障碍树中隐藏,关闭时再恢复,但强烈不建议这么做,优先考虑调整结构。

第二步:通知辅助软件把焦点移进弹窗

只设 accessibilityViewIsModal = YES 还不够。它解决的是“底层不该被访问”的问题,但解决不了“焦点还在原来的按钮上”的问题。用户激活“筛选”按钮后,VoiceOver焦点还停在那个按钮上,不知道弹窗已经出现了。

这时候需要发送一个通知:

UIAccessibilityPostNotification(
    UIAccessibilityScreenChangedNotification,
    filterPanel.accessibilityElements.firstObject
);

UIAccessibilityScreenChangedNotification 用于通知系统“屏幕内容发生了重大变化”。根据 iOS 文档描述它适用的场景是“当一个新的视图出现并占据屏幕主要部分时”。通知的第二个参数指定了焦点应该移动到的目标元素。如果传nil,VoiceOver会聚焦到页面上的第一个可聚焦元素。

这就解决了焦点没有自动聚焦到弹窗内部的问题。用户打开弹窗后,VoiceOver会立即朗读传入的那个元素。

六、 几个容易踩的坑

Toast不能设成模态。 accessibilityViewIsModal = YES 的语义是“当前上下文被阻塞,用户必须先处理这个视图才能继续”。Toast是非阻塞式提示,用户看到提示的同时仍然可以操作页面其他内容。如果给Toast设了模态,VoiceOver用户会被困在Toast里,体验反而更差。此前,react-native-screens库就因为这个原因专门回退过一个改动。

关闭时忘记设回NO。 如果不把 accessibilityViewIsModal 设回 NO,弹窗虽然移除了,但底层内容可能仍然被VoiceOver忽略。

通知一定要在主线程发,且要在布局完成之后。 在 viewDidLoad 里发通知是无效的,因为视图还没上屏。建议在 layoutIfNeeded 之后,或者等动画完成的回调里发送。

焦点目标不要传nil。 虽然 Apple 文档表示,传nil会聚焦到第一个可聚焦元素,但“第一个可聚焦元素”是系统按层级和元素位置算出来的,不一定是弹窗中的真正想要用户操作的第一个元素。最好显式传一个确定的目标,比如弹窗标题或第一个可操作元素。

七、总结

底层焦点这个问题,说到底就是开发者没有把“视觉模态”和“无障碍模态”在语义上对齐。视觉上弹窗盖住了底层内容,但无障碍树里底层元素还在,VoiceOver就会继续读。

无障碍开发就是这样,API看起来简单,坑都在细节里。希望这篇文章能帮到正在做iOS无障碍优化的伙伴们。如果大家遇到类似问题,欢迎一起交流讨论。

参考资料

  1. Apple Developer Documentation: accessibilityViewIsModal

  2. Apple Developer Documentation: UIAccessibilityScreenChangedNotification


作者

张赐荣,视障者,信息无障碍软件优化解决方案资深研发工程师。

深耕Web/PC/移动端可访问性工作多年,对跨平台无障碍解决方案拥有独特的理论研究和丰富的实战经验。

精通视障软件交互设计,致力于改善、提升产品可及性体验。

posted on 2026-09-13 14:53  张赐荣  阅读(29)  评论(0)    收藏  举报

感谢您访问张赐荣的技术分享博客!
博客地址:https://cnblogs.com/netlog/
知乎主页:https://www.zhihu.com/people/tzujung-chang
个人网站:https://prc.cx/