iOS 27 的无障碍设计:阅读类 App 与自定义控件
阅读长篇内容与在界面中导航是截然不同的问题:前者的目标是在文本间流畅移动,而非在控件之间跳转。WWDC26 的两场无障碍议程正是沿着这道接缝一分为二:一场谈阅读界面,一场谈环绕其周围的控件。
这样的划分很重要,因为两者的修正方式在本质上就不同。阅读类 App 的问题出在连续性:文本无法跨段落衔接、朗读整篇时停在页面底部。自定义控件的问题则出在转译:某个手势在视觉上传达了一切,却没能把任何信息传给 VoiceOver。iOS 27 同时为这两种情况推出了 API,其中 accessibilityLinkedGroup 是今年全新登场的。
TL;DR
- 阅读类 App 应优先选用系统文本视图。启用选择功能的
UITextView、TextEditor与Text都采用了UITextInput协议,可免费获得逐行/逐字/逐字符的导航与选择功能1。 - 当布局迫使文本元素分开时,请将它们链接起来,让 VoiceOver 能跨越接缝移动。iOS 18 引入了
accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElement;iOS 27 则新增了 SwiftUI 的accessibilityLinkedGroup修饰符来达成相同效果1。 - 对于分页内容,
causesPageTurn特征搭配accessibilityScroll,能让“朗读屏幕”与 VoiceOver 在朗读整篇时自动翻页1。 - 自行渲染的文本(扫描页面、高级排版)会丧失上述所有功能。完整采用
UITextInput即可将其恢复:通过selectionRects提供几何信息、通过textInRange提供子字符串,并以 tokenizer 处理逐行/逐字/逐字符的导航1。 - 自定义控件遵循四项指导原则:用途、数值、动作、反馈。对应的工具为
accessibilityLabel/accessibilityValue、搭配accessibilityAdjustableAction的.adjustable特征、用于多轴控件的自定义动作,以及用于手势密集界面的直接触摸(allowsDirectInteraction)2。
阅读类 App:衔接被布局拆散的文本
这场谈阅读类 App 的议程,围绕着一个看似简单的约束条件展开。讲者的旅游指南 App 因布局需求,为每个段落使用各自独立的 UITextView,而非以单一视图容纳整个页面1。每个独立的文本视图本身都具备无障碍能力,问题却出现在它们彼此的交界处。
讲者为这款 App 设定了三个目标:细致的文本导航,让 VoiceOver 与“朗读屏幕”能流畅地穿行于文本之间;不受打断的连续阅读体验;以及完整的文本选择1。议程的其余部分,便是逐一审视哪个 API 能满足哪个目标。
要跨越独立视图进行导航,答案是 iOS 18 引入的文本导航元素 API。针对每个文本元素,你要返回 VoiceOver 接下来应移往的下一个与上一个无障碍文本元素。在议程的示例中,段落 1 通过自身的 accessibilityNextTextNavigationElement 返回段落 2,段落 2 则通过自身的 accessibilityPreviousTextNavigationElement 返回段落 11。一旦接好线,VoiceOver 就会越过某段的结尾、进入下一段的第一行,而不再播放走到死路的提示音。
iOS 27 的新增功能落在 SwiftUI。如讲者所言,从 iOS 27 开始,使用 accessibilityLinkedGroup 修饰符将多个文本元素链接在一起,即可达成相同效果1。你为链接的元素赋予相同的 id 与 namespace,它们便会继承跨元素的文本导航行为,无需手动维护下一个/上一个的对应关系。AppKit 则提供对应的 accessibilitySharedTextUIElements,在 Mac 上取得相同结果1。本系列的《SwiftUI 的组成》一文,说明了像这样的 SwiftUI 修饰符如何向下解析到底层的无障碍树。
连续性是第二个目标。分页内容需要滑动,而朗读整篇时应像有声书一样忽略页面边界。在议程中,“朗读屏幕”在第一页底部戛然而止,直到讲者为每一页的最后一个段落应用 causesPageTurn 特征。搭配 accessibilityScroll 后,“朗读屏幕”与 VoiceOver 一抵达结尾便会自动滚动到下一页;此特征在 UIKit 与 SwiftUI 中均可使用1。
第三个目标——选择——在使用系统文本视图时大多能自动取得,但议程加入了一个用心的巧思:一个通过 VoiceOver 编辑转子提供的“保存推荐”动作。讲者重写了段落文本视图的 accessibilityCustomActions,并以 edit 类别创建此自定义动作,正是为了让它出现在编辑转子中、与文本选择操作并列,而非沦为一般动作1。指引相当明确:当自定义动作与文本选择相关时,请使用 edit 类别。
当你自行渲染文本时:完整的 UITextInput
阅读议程的后半段,处理的是系统视图不可行的情况。自行渲染的文本会出现在讲求高级排版的专业阅读类 App、跨 App 共享的代码,或扫描页面之中,而讲者的示例最为极端:他把旅游指南的文本视图,换成从手写笔记本扫描进来的页面。代价是全面性的。改用图像后,便丧失了 UITextView 免费提供的无障碍行为,连最基本的——把文本朗读出来——都不剩。VoiceOver 只会说“图像”1。
解法是采用 UITextInput 协议,它可附加在任何无障碍元素上,让渲染文本或图像中的文本,变得跟标准文本视图一样无障碍1。议程直言其中的关键:你必须完整实现它,才能获得完整的好处。讲者逐一讲解了几个关键环节:
- 几何信息。
selectionRects为指定范围计算高亮矩形。讲者以手写图像为基础,利用每一行已知的高度与宽度,通过自定义的selectionRectFromImage函数近似算出矩形,再返回组合好的数组1。 - 子字符串。
textInRange只返回辅助技术所查询的那一段文本1。 - tokenizer。 逐行、逐句、逐字或逐字符的导航,均通过 tokenizer 处理。议程将 UIKit 的
UITextInputStringTokenizer子类化,以配合自定义的布局1。
有一项细节调整明确属于可选。为了让选择带有控制柄与高亮、感觉更完整,讲者为页面视图加上一个 UITextInteraction,并在选择变动时调用输入委托,好让系统更新视觉呈现。议程指出,UITextInput 本身并不要求这个步骤;它只是把体验修饰得更圆满,以贴近标准文本视图1。而且 UITextInput 可与先前的 API 组合,因此 causesPageTurn 与导航元素在自行渲染的文本上同样有效。
议程点出了一项容易被低估的回报:做这些工作不只服务 VoiceOver 与“朗读屏幕”。自 iOS 26 起,无障碍阅读器能以更利于阅读的版面打开 App 的内容,而同样这套无障碍文本的做法,也能改善那种体验1。
自定义控件:用途、数值、动作、反馈
这场谈自定义控件的议程,以一个标准的 SwiftUI 滑块开场,并论证它为何行得通。你一眼就能读出一条轨道、一个位于中间的把手、一个可拖动的暗示,以及即时的反馈。没有人解释过其中任何一项。接着议程提出显而易见的问题:要是有人看不到屏幕呢?VoiceOver 的回应是朗读“亮度,50%,可调整”,再加上向上或向下滑动的提示——这传达了视觉上呈现的同样四件事:用途、数值、可用的动作,以及数值变动时的反馈2。
.adjustable 特征,把它从干巴巴的“按钮,6 盎司”变成 VoiceOver 可操控的可调整滑块。
这四个词(用途、数值、动作、反馈)就是议程的指导原则,每个示例都会回扣到它们2。第一个是咖啡分配器控件:向上拖动出更多咖啡、向下拖动出更少,填充程度代表盎司数。在任何处理之前,VoiceOver 只把它读成一般的“按钮,6 盎司”,完全没提示该如何改变数值2。修正是逐步进行的:
- 用途与数值。
accessibilityLabel将它命名为“咖啡分配器”;accessibilityValue则播报当前的填充量2。 - 动作。
.adjustable特征告诉 VoiceOver 此控件会响应上/下滑动,而accessibilityAdjustableAction提供一个带有方向参数(.increment或.decrement)的闭包,以处理各自的情况2。
这样便能一次调整一盎司。为了更细致的控制,议程动用了 VoiceOver 内置的穿透手势:一个从控件 accessibilityActivationPoint 起始的双击并按住,并在手指移动时把触摸事件直接送往控件。讲者把激活点设定为对应当前的填充程度2。穿透期间的反馈是一堂谈克制的小课:议程只在数值确实有所变动、且至少经过 0.3 秒时才发出播报,因为每次变动都播报会太吵2。
均衡器面板则把标准拉得更高。它是二维控件,议程坦言 .adjustable 并非合适的工具,因为它的递增/递减动作只涵盖单一轴向。答案是自定义动作:将 accessibilityAction 修饰符应用四次,分别对应“上移”“右移”“下移”“左移”,每次把一个轴向推移固定的一步,并限制在范围之内2。与可调整动作不同的是,自定义动作支持你所定义的任何操作,而且也能服务切换控制与语音控制的用户2。
直接触摸:当手势本身就是重点
议程的最后一个示例是一个虚拟猫咪控件,你可以抚摸、轻点与捏掐以触发不同反应。讲者指出,穿透在此并不合适,因为人们可能想一再重复某个动作、或使用多种手势2。于是这个控件改采直接触摸。
.requiresActivation 的 .accessibilityDirectTouch 修饰符,让触摸直接穿透到猫咪,而不被 VoiceOver 拦截。
allowsDirectInteraction 特征会把某个区域标记为直接触摸区:触摸事件直接送往控件,而不经 VoiceOver 处理,因此控件所支持的每一个手势都能运作2。有两个选项可塑造其行为。.requiresActivation 会让控件在双击之前保持静止,使人能在屏幕上拖动而不会误触;此后直接触摸会一直生效,直到焦点离开该元素为止。.silentOnTouch 则让 VoiceOver 在该区域保持安静,适用于那些会自行发出声音、否则会被 VoiceOver 语音盖过的控件2。这只虚拟猫咪使用的是搭配 .requiresActivation 的 .accessibilityDirectTouch2。
议程以一则告诫作结,呼应《无障碍即平台》中关于平台无障碍的论点:并非人人都能执行直接触摸手势,因此请尽可能提供另一条途径,例如自定义动作,让切换控制与语音控制的用户也能触及同样的交互2。
采用指引
两场议程都以同一句叮嘱收尾:打开 VoiceOver,亲自审视你自己的 App。具体而言:
- 对于建立在系统文本视图上的阅读界面,试试朗读整篇的手势、以行转子导航,并选择文本。若朗读整篇停在页面边界,请采用
causesPageTurn搭配accessibilityScroll。若逐行导航在独立的文本元素之间走进死路,请以导航元素 API(UIKit)或accessibilityLinkedGroup(SwiftUI,iOS 27)将它们链接起来1。 - 如果你自行渲染文本,请规划完整采用
UITextInput,而非局部采用;这个协议是全有或全无,而可选的UITextInteraction步骤,正是让选择感觉起来像原生的关键1。 - 对于任何自定义控件,依序走过这四项原则。VoiceOver 用户能不能辨别它是什么(标签)、它处于什么状态(数值)、他们能做什么(adjustable 特征或自定义动作),以及发生了什么(播报)?把直接触摸保留给那些“数值即手势本身”的控件,并为它搭配一条非手势的兜底途径2。
反复出现的主题是:先选用系统组件,并把自定义路线视为需要扎实、完整投入的例外。《一款 iOS App 的三个界面》一文,把无障碍设计定位为与可见界面、App Intents 并列的一级界面;这两场议程,正展现了把这个界面在文本与控件上做对,是什么模样。
常见问题
iOS 27 为阅读类 App 推出的新无障碍 API 是什么?
SwiftUI 的 accessibilityLinkedGroup 修饰符。从 iOS 27 开始,将多个文本元素以相同的 id 与 namespace 链接起来,便能赋予它们跨元素的文本导航,让 VoiceOver 从某个元素的最后一行移往下一个元素的第一行。它是 iOS 18 的 accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElement API,以及 AppKit accessibilitySharedTextUIElements 在 SwiftUI 中的对应做法1。
如果我使用标准文本视图,还需要实现 UITextInput 吗?
不需要。UITextView(UIKit)、TextEditor 与启用选择的 Text(SwiftUI),以及 NSTextView(AppKit)都已采用 UITextInput,开箱即提供逐行/逐字/逐字符的导航与选择。只有当你自行渲染文本——例如扫描页面或高级排版——导致那些系统行为丧失时,才需要自己采用 UITextInput1。
自定义控件何时该用 adjustable 特征、何时该用自定义动作?
对于递增与递减有意义的单轴数值,例如滑块,请使用搭配 accessibilityAdjustableAction 的 .adjustable 特征。当单一轴向不够用时,例如二维面板,或当你想公开让 VoiceOver 逐一念出名称的离散操作时,请使用自定义动作(accessibilityAction 修饰符)。议程的均衡器面板之所以使用四个自定义动作(上/右/下/左移),正是因为 adjustable 特征只涵盖一个方向2。
什么是直接触摸?我何时该使用它?
直接触摸(allowsDirectInteraction 特征,在 SwiftUI 中通过 .accessibilityDirectTouch 应用)会把某个区域标记成让触摸直接送往你的控件,而不经 VoiceOver 处理,使人能使用控件所支持的每一个手势。请在穿透手势并不合适的手势密集控件上使用它,并搭配 .requiresActivation 以防止误触。对于无法执行直接触摸手势的人,请务必提供一条非手势的兜底途径,例如自定义动作2。
无障碍文本如何惠及 VoiceOver 以外的功能?
同样的工作也能在“朗读屏幕”上回收成效;而且自 iOS 26 起,无障碍阅读器能以更利于阅读的版面打开 App 的内容。实现阅读议程所涵盖的文本导航、翻页与 UITextInput 做法,只需一套改动,便能同时改善这三种体验1。
延伸阅读
- 无障碍即平台:Personal Voice、实时语音、眼动追踪、音乐触感反馈
- SwiftUI 的组成
- 一款 iOS App 的三个界面
- iOS 26 的 Widget 与控件界面
- 系列主页:Apple Ecosystem 系列
- 指南:iOS Agent 开发
参考资料
-
Apple,WWDC26 议程 219,”Enhance the accessibility of your reading app.”。developer.apple.com/videos/play/wwdc2026/219。系统文本视图(
UITextView、TextEditor、启用选择的Text、NSTextView)采用UITextInput;iOS 18 的accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElementAPI 与 iOS 27 SwiftUI 的accessibilityLinkedGroup修饰符(AppKit:accessibilitySharedTextUIElements);causesPageTurn搭配accessibilityScroll;通过带 edit 类别的accessibilityCustomActions提供的文本选择动作;为自行渲染文本完整采用UITextInput(selectionRects、textInRange、UITextInputStringTokenizer)及可选的UITextInteraction;以及 iOS 26 无障碍阅读器,均以此为出处。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple,WWDC26 议程 220,”Refine accessibility for custom controls.”。developer.apple.com/videos/play/wwdc2026/220。用途/数值/动作/反馈原则;使用
accessibilityLabel、accessibilityValue、.adjustable特征与accessibilityAdjustableAction的咖啡分配器控件;在accessibilityActivationPoint上、带节流播报(数值变动加上经过 0.3 秒)的穿透手势;用于均衡器面板的accessibilityAction修饰符;以及为虚拟猫咪控件通过allowsDirectInteraction(.accessibilityDirectTouch)搭配.requiresActivation与.silentOnTouch实现的直接触摸,连同非手势兜底途径的提醒,均以此为出处。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩