Highlight
Apple 为阅读类 App 提供了文本导航 API(
accessibilityNextTextNavigationElement和accessibilityLinkedGroup)和UITextInput协议,使离散文本视图和自定义渲染文本都能获得 VoiceOver 逐行导航、连续朗读翻页和文本选择的原生体验。
核心内容
做阅读类 App 的开发者常遇到一个问题:页面由多个独立文本视图拼接而成时,VoiceOver 读到段落末尾就”撞墙”了。用户必须重新寻找下一个焦点,阅读体验被硬生生打断。
(06:43)演讲者用旅行指南 App 演示了这个场景。每个段落是一个独立的 UITextView,VoiceOver 的 Lines 转子(rotor)在段落末尾卡住,发出错误提示音。用户无法按行流畅地跨段落阅读。
Apple 的解决思路分两条线。第一条针对标准文本视图:用导航 API 把离散的文本元素在辅助功能树里”缝合”起来。第二条针对自定义渲染文本:完整实现 UITextInput 协议,让扫描图片、手写笔记这类非标准内容也能获得原生文本体验。
跨段落文本导航
(07:00)iOS 18 引入了文本导航 API。在 UIKit 中,为每个文本元素设置 accessibilityNextTextNavigationElement 和 accessibilityPreviousTextNavigationElement,VoiceOver 就能在段落间无缝移动。
(08:02)SwiftUI 在 iOS 27 提供了更简洁的方案:accessibilityLinkedGroup(id:in:) 修饰符。同一命名空间(namespace)下相同 ID 的文本元素会自动获得跨元素导航能力。
连续阅读与自动翻页
(09:35)分页内容在 Speak Screen 朗读时会在页尾停止。Apple 提供了 .causesPageTurn trait,配合 accessibilityScroll 方法,让辅助技术在读完一页后自动翻页,体验接近有声书。
文本选择与自定义操作
(11:16)标准文本视图已支持 VoiceOver 的文本选择。开发者还可以把业务操作(如”保存推荐”)注册到编辑转子(edit rotor)中,让用户在选择文本后直接执行。
自定义文本的辅助功能
(14:38)当 App 使用扫描图片、自定义渲染等无法使用标准文本视图的场景时,完整实现 UITextInput 协议可以让这些内容获得与原生文本视图相同的辅助功能体验:逐行触摸探索、转子导航、文本选择。
详细内容
用 UIKit 连接离散文本视图
(07:29)
import UIKit
class TravelGuidePageController: UIViewController {
var paragraphs: [TravelGuideParagraph]
func configureNavigationElements() {
for (index, paragraph) in paragraphs.enumerated() {
if index + 1 < paragraphs.count {
paragraph.accessibilityNextTextNavigationElement = paragraphs[index + 1]
}
if index - 1 >= 0 {
paragraph.accessibilityPreviousTextNavigationElement = paragraphs[index - 1]
}
}
}
}
关键点:
accessibilityNextTextNavigationElement指定 VoiceOver 按行导航时的下一个目标元素accessibilityPreviousTextNavigationElement指定上一个目标元素- 在控制器初始化时遍历所有段落,为每个段落设置双向链接
- 边界段落(第一个和最后一个)只需设置单向链接
用 SwiftUI 链接文本组
(07:59)
import SwiftUI
struct PageView: View {
@Namespace private var pageNamespace
var paragraphs: [String]
var pageNumber: Int
var body: some View {
Text(paragraphs[0])
.textSelection(.enabled)
.accessibilityLinkedGroup(id: pageNumber, in: pageNamespace)
Text(paragraphs[1])
.textSelection(.enabled)
.accessibilityLinkedGroup(id: pageNumber, in: pageNamespace)
}
}
关键点:
@Namespace创建一个命名空间,用于限定链接组的作用范围accessibilityLinkedGroup(id:in:)的id相同且namespace相同的文本元素会被视为一个连续的文本块.textSelection(.enabled)开启文本选择,是获得完整辅助功能体验的前提- AppKit 用户可使用
accessibilitySharedTextUIElements达到类似效果
自动翻页
(09:50)
import UIKit
class TravelGuidePageController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.lastParagraphView.accessibilityTraits.insert(.causesPageTurn)
}
override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
moveToPage(direction)
var scrollString = "Page \(currentPage) of \(pages.count)"
UIAccessibility.post(notification: .pageScrolled, argument: scrollString)
return true
}
}
关键点:
.causesPageTurntrait 标记页面最后一个元素,Speak Screen 读到这里会自动触发翻页accessibilityScroll(_:)处理翻页逻辑,返回true表示翻页成功UIAccessibility.post(notification: .pageScrolled, argument:)播报当前页码,让用户知道翻页了- 这个 trait 在 UIKit 和 SwiftUI 中都可用
编辑转子自定义操作
(11:45)
import UIKit
class TravelGuideParagraph: UITextView {
override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
get {
let saveAction = UIAccessibilityCustomAction(name: "Save Recommendation") { _ in
self.saveRecommendation()
}
saveAction.category = UIAccessibilityCustomAction.editCategory
return (super.accessibilityCustomActions ?? []) + [saveAction]
}
set { }
}
private func saveRecommendation() -> Bool {
// 保存选中的推荐内容
return true
}
}
关键点:
UIAccessibilityCustomAction.editCategory把操作注册到编辑转子,而非通用操作菜单- 用户在文本选择状态下通过转子切换模式,能找到这个操作
- 必须保留
super.accessibilityCustomActions,避免覆盖系统默认操作 - 操作闭包返回
Bool,表示操作是否成功执行
自定义视图实现 UITextInput
(16:10)
import UIKit
class ScannedPage: UIView, UITextInput {
override init(frame: CGRect) {
super.init(frame: frame)
let interaction = UITextInteraction(for: .nonEditable)
interaction.textInput = self
addInteraction(interaction)
}
func selectionRects(for range: UITextRange) -> [UITextSelectionRect] {
var rects: [UITextSelectionRect] = []
let startLine = lineIndex(for: range.start)
let endLine = lineIndex(for: range.end)
for line in startLine...endLine {
let rect = selectionRectFromImage(for: range, in: line)
rects.append(rect)
}
return rects
}
func text(in range: UITextRange) -> String? {
let nsRange = nsRange(from: range)
guard let range = Range(nsRange, in: scannedText) else {
return nil
}
return String(scannedText[range])
}
var tokenizer: any UITextInputTokenizer {
CustomHandwritingTokenizer(textInput: self)
}
weak var inputDelegate: UITextInputDelegate?
var selectedTextRange: UITextRange? {
willSet { inputDelegate?.selectionWillChange(self) }
didSet { inputDelegate?.selectionDidChange(self) }
}
}
关键点:
UITextInteraction(for: .nonEditable)为不可编辑文本添加系统级选择交互,自动显示选择手柄和高亮selectionRects(for:)返回选中区域的矩形数组,VoiceOver 用这些矩形绘制焦点框text(in:)返回指定范围的文本内容,辅助技术用这个方法获取要朗读的文本tokenizer提供分词器,管理按行、句子、词、字符的导航粒度selectedTextRange的willSet和didSet通知系统选择变化,触发视觉更新UITextInput需要完整实现所有方法才能获得全部辅助功能体验
核心启发
为小说阅读器添加跨章节连续朗读
做什么:用 accessibilityLinkedGroup 或导航 API 把段落串起来,再为章节末尾设置 .causesPageTurn,让 VoiceOver 和 Speak Screen 连续播放整章内容。
为什么值得做:阅读器通常把每章拆成多个段落视图做懒加载,VoiceOver 读到段落末尾就”撞墙”了,用户必须重新寻找下一个焦点。跨段落导航让视力障碍用户获得接近有声书的连续阅读体验。
怎么开始:SwiftUI 用 accessibilityLinkedGroup(id:in:),UIKit 用 accessibilityNextTextNavigationElement 和 accessibilityPreviousTextNavigationElement 连接离散文本视图。
让扫描文档 App 支持文本选择
做什么:扫描版 PDF 或手写笔记完整实现 UITextInput 协议,让图片里的文字也能被 VoiceOver 逐行朗读、被 Speak Screen 连续阅读,还能被用户选中复制。
为什么值得做:扫描文档对视力障碍用户一直是”死物”,无法交互。实现 UITextInput 后,这些非标准内容获得与原生文本视图相同的辅助功能体验。
怎么开始:让自定义视图遵循 UITextInput 协议,添加 UITextInteraction(for: .nonEditable),实现 selectionRects(for:)、text(in:) 和 tokenizer。
在文本编辑场景添加业务操作
做什么:笔记类 App 给选中的文本添加”加入收藏""生成摘要”等操作,把这些操作注册到编辑转子。
为什么值得做:视力障碍用户在选择文本后,如果要在复杂的菜单里翻找业务操作,体验很差。编辑转子把常用操作放在文本选择后的快捷入口,减少操作路径。
怎么开始:创建 UIAccessibilityCustomAction,设置 category 为 UIAccessibilityCustomAction.editCategory,在 accessibilityCustomActions 中返回。
为图文混排页面提供无障碍体验
做什么:杂志类 App 的页面常有文字环绕图片的复杂排版。把文本块用 accessibilityLinkedGroup 链接,图片单独设置描述标签。
为什么值得做:复杂排版下辅助技术可能按错误的顺序遍历内容,图片没有描述标签时会被跳过。正确的链接和标签让整个页面能被按阅读顺序完整遍历。
怎么开始:给文本视图添加 accessibilityLinkedGroup(id:in:) 保持阅读连续性,给图片设置 accessibilityLabel 描述内容。
测试 App 的无障碍阅读体验
做什么:打开 VoiceOver,用 Lines 转子逐行滑动,验证是否能跨段落连续移动。打开 Speak Screen(双指从屏幕顶部下滑),检查朗读是否在页尾自动翻页。
为什么值得做:辅助功能问题往往在正常视觉测试中被忽略。每次 UI 结构调整后都可能破坏无障碍体验,定期测试能及早发现问题。
怎么开始:在设备设置中开启 VoiceOver 和 Speak Screen,用 Lines 转子测试跨段落导航,用 .causesPageTurn 验证自动翻页。
关联 Session
- 220 - Accessibility Controls — 介绍新的辅助功能控制 API,与阅读体验的交互控制互补
- 370 - TextKit — 深入文本渲染和排版,自定义文本实现
UITextInput时需要了解的几何计算基础 - 278 - UIKit modernization — UIKit 的新特性和现代化实践,包含辅助功能相关改进
- 269 - SwiftUI — SwiftUI 的新 API,包含
accessibilityLinkedGroup等辅助功能修饰符的更多用法
评论
GitHub Issues · utterances