← 所有文章

iOS 27 的无障碍设计:阅读类 App 与自定义控件

阅读长篇内容与在界面中导航是截然不同的问题:前者的目标是在文本间流畅移动,而非在控件之间跳转。WWDC26 的两场无障碍议程正是沿着这道接缝一分为二:一场谈阅读界面,一场谈环绕其周围的控件。

这样的划分很重要,因为两者的修正方式在本质上就不同。阅读类 App 的问题出在连续性:文本无法跨段落衔接、朗读整篇时停在页面底部。自定义控件的问题则出在转译:某个手势在视觉上传达了一切,却没能把任何信息传给 VoiceOver。iOS 27 同时为这两种情况推出了 API,其中 accessibilityLinkedGroup 是今年全新登场的。

TL;DR

  • 阅读类 App 应优先选用系统文本视图。启用选择功能的 UITextViewTextEditorText 都采用了 UITextInput 协议,可免费获得逐行/逐字/逐字符的导航与选择功能1
  • 当布局迫使文本元素分开时,请将它们链接起来,让 VoiceOver 能跨越接缝移动。iOS 18 引入了 accessibilityNextTextNavigationElementaccessibilityPreviousTextNavigationElement;iOS 27 则新增了 SwiftUI 的 accessibilityLinkedGroup 修饰符来达成相同效果1
  • 对于分页内容,causesPageTurn 特征搭配 accessibilityScroll,能让“朗读屏幕”与 VoiceOver 在朗读整篇时自动翻页1
  • 自行渲染的文本(扫描页面、高级排版)会丧失上述所有功能。完整采用 UITextInput 即可将其恢复:通过 selectionRects 提供几何信息、通过 textInRange 提供子字符串,并以 tokenizer 处理逐行/逐字/逐字符的导航1
  • 自定义控件遵循四项指导原则:用途、数值、动作、反馈。对应的工具为 accessibilityLabelaccessibilityValue、搭配 accessibilityAdjustableAction.adjustable 特征、用于多轴控件的自定义动作,以及用于手势密集界面的直接触摸(allowsDirectInteraction2

阅读类 App:衔接被布局拆散的文本

这场谈阅读类 App 的议程,围绕着一个看似简单的约束条件展开。讲者的旅游指南 App 因布局需求,为每个段落使用各自独立的 UITextView,而非以单一视图容纳整个页面1。每个独立的文本视图本身都具备无障碍能力,问题却出现在它们彼此的交界处。

Watch on Apple Developer ↗
Apple 演示 VoiceOver 卡在单一段落内逐行导航、无法跨入下一段的情况,原因是每个段落都是独立的视图,接着再以文本导航元素的 API 将它们链接起来。

讲者为这款 App 设定了三个目标:细致的文本导航,让 VoiceOver 与“朗读屏幕”能流畅地穿行于文本之间;不受打断的连续阅读体验;以及完整的文本选择1。议程的其余部分,便是逐一审视哪个 API 能满足哪个目标。

要跨越独立视图进行导航,答案是 iOS 18 引入的文本导航元素 API。针对每个文本元素,你要返回 VoiceOver 接下来应移往的下一个与上一个无障碍文本元素。在议程的示例中,段落 1 通过自身的 accessibilityNextTextNavigationElement 返回段落 2,段落 2 则通过自身的 accessibilityPreviousTextNavigationElement 返回段落 11。一旦接好线,VoiceOver 就会越过某段的结尾、进入下一段的第一行,而不再播放走到死路的提示音。

iOS 27 的新增功能落在 SwiftUI。如讲者所言,从 iOS 27 开始,使用 accessibilityLinkedGroup 修饰符将多个文本元素链接在一起,即可达成相同效果1。你为链接的元素赋予相同的 idnamespace,它们便会继承跨元素的文本导航行为,无需手动维护下一个/上一个的对应关系。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

Watch on Apple Developer ↗
Apple 为自定义的咖啡分配器控件加上标签、数值,以及搭配可调整动作的 .adjustable 特征,把它从干巴巴的“按钮,6 盎司”变成 VoiceOver 可操控的可调整滑块。

这四个词(用途、数值、动作、反馈)就是议程的指导原则,每个示例都会回扣到它们2。第一个是咖啡分配器控件:向上拖动出更多咖啡、向下拖动出更少,填充程度代表盎司数。在任何处理之前,VoiceOver 只把它读成一般的“按钮,6 盎司”,完全没提示该如何改变数值2。修正是逐步进行的:

  1. 用途与数值。 accessibilityLabel 将它命名为“咖啡分配器”;accessibilityValue 则播报当前的填充量2
  2. 动作。 .adjustable 特征告诉 VoiceOver 此控件会响应上/下滑动,而 accessibilityAdjustableAction 提供一个带有方向参数(.increment.decrement)的闭包,以处理各自的情况2

这样便能一次调整一盎司。为了更细致的控制,议程动用了 VoiceOver 内置的穿透手势:一个从控件 accessibilityActivationPoint 起始的双击并按住,并在手指移动时把触摸事件直接送往控件。讲者把激活点设定为对应当前的填充程度2。穿透期间的反馈是一堂谈克制的小课:议程只在数值确实有所变动、且至少经过 0.3 秒时才发出播报,因为每次变动都播报会太吵2

均衡器面板则把标准拉得更高。它是二维控件,议程坦言 .adjustable 并非合适的工具,因为它的递增/递减动作只涵盖单一轴向。答案是自定义动作:将 accessibilityAction 修饰符应用四次,分别对应“上移”“右移”“下移”“左移”,每次把一个轴向推移固定的一步,并限制在范围之内2。与可调整动作不同的是,自定义动作支持你所定义的任何操作,而且也能服务切换控制与语音控制的用户2

直接触摸:当手势本身就是重点

议程的最后一个示例是一个虚拟猫咪控件,你可以抚摸、轻点与捏掐以触发不同反应。讲者指出,穿透在此并不合适,因为人们可能想一再重复某个动作、或使用多种手势2。于是这个控件改采直接触摸。

Watch on Apple Developer ↗
Apple 为这个以手势驱动的控件加上带 .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 开始,将多个文本元素以相同的 idnamespace 链接起来,便能赋予它们跨元素的文本导航,让 VoiceOver 从某个元素的最后一行移往下一个元素的第一行。它是 iOS 18 的 accessibilityNextTextNavigationElementaccessibilityPreviousTextNavigationElement 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

延伸阅读

参考资料


  1. Apple,WWDC26 议程 219,”Enhance the accessibility of your reading app.”。developer.apple.com/videos/play/wwdc2026/219。系统文本视图(UITextViewTextEditor、启用选择的 TextNSTextView)采用 UITextInput;iOS 18 的 accessibilityNextTextNavigationElementaccessibilityPreviousTextNavigationElement API 与 iOS 27 SwiftUI 的 accessibilityLinkedGroup 修饰符(AppKit:accessibilitySharedTextUIElements);causesPageTurn 搭配 accessibilityScroll;通过带 edit 类别的 accessibilityCustomActions 提供的文本选择动作;为自行渲染文本完整采用 UITextInputselectionRectstextInRangeUITextInputStringTokenizer)及可选的 UITextInteraction;以及 iOS 26 无障碍阅读器,均以此为出处。 

  2. Apple,WWDC26 议程 220,”Refine accessibility for custom controls.”。developer.apple.com/videos/play/wwdc2026/220。用途/数值/动作/反馈原则;使用 accessibilityLabelaccessibilityValue.adjustable 特征与 accessibilityAdjustableAction 的咖啡分配器控件;在 accessibilityActivationPoint 上、带节流播报(数值变动加上经过 0.3 秒)的穿透手势;用于均衡器面板的 accessibilityAction 修饰符;以及为虚拟猫咪控件通过 allowsDirectInteraction.accessibilityDirectTouch)搭配 .requiresActivation.silentOnTouch 实现的直接触摸,连同非手势兜底途径的提醒,均以此为出处。 

相关文章

iOS 27 中的 SwiftUI 性能与互操作

iOS 27 的 SwiftUI 如何处理惰性栈滚动、GPU 着色器效果以及 AppKit/UIKit 互操作,取材于三场官方 WWDC26 UI Frameworks 主题讲座。

7 分钟阅读

iOS 27 中 SwiftUI 的新变化

iOS 27 重构了 SwiftUI 的列表、文档、工具栏与错误处理:拖拽重排序、可读写的文档模型、工具栏溢出,以及基于条目的弹窗。

10 分钟阅读

设计工程师的Agent技术栈

设计工程师需要能够强制执行视觉一致性、字体排版规范、色彩合规性和审美品味的Agent基础设施。以下是六大核心组件。

1 分钟阅读