iOS:VoiceOver模式下UITextView链接未出现在转子无法激活问题
UITextView 默认对 URL 链接的 VoiceOver 支持是基于系统内置的 UITextItemInteraction 机制实现的,当你手动覆写了点击/手势事件,或者修改了 isEditable、isSelectable 的默认配置、或者实现了 UITextViewDelegate 的 shouldInteractWith 方法返回了 false,都会打断系统内置的无障碍元素解析流程,导致 VoiceOver 无法将文本中的 URL 识别为独立的可交互无障碍节点。
方案1:优先保留系统默认交互逻辑,仅在自定义逻辑处做分流
不要直接全局覆写 UITextView 的点击手势,而是通过UITextViewDelegate的textView(_:shouldInteractWith:in:interaction:)方法处理链接交互,同时保留系统的无障碍解析能力。如果需要区分常规模式和 VoiceOver 模式的交互,可以通过UIAccessibility.isVoiceOverRunning动态判断返回值:func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { // 执行自定义链接处理逻辑 yourCustomHandleURLMethod(URL) // 返回false阻断系统默认跳转,返回true保留系统默认行为 return false }注意要保证
UITextView的isSelectable = true、isEditable = false,这两个配置是系统识别文本链接的前提。方案2:手动添加无障碍节点适配 VoiceOver 场景
如果必须全局覆写点击手势实现自定义逻辑,需要手动将文本中的 URL 对应的区域注册为独立的无障碍元素,让 VoiceOver 可以识别到:
首先遍历UITextView的 attributedText 中的所有NSLinkAttributeName属性,获取每个链接的文本范围、对应的 URL、在视图中的 frame,然后创建UIAccessibilityElement实例添加到 textView 的accessibilityElements数组中:// 遍历属性文本获取所有链接 textView.attributedText.enumerateAttribute(.link, in: NSRange(location: 0, length: textView.attributedText.length), options: []) { value, range, stop in guard let url = value as? URL else { return } // 获取链接文本对应的视图frame let glyphRange = textView.layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) let linkRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textView.textContainer) // 转换为textView的坐标系 let convertedRect = linkRect.offsetBy(dx: textView.textContainerInset.left, dy: textView.textContainerInset.top) // 创建无障碍元素 let linkElement = UIAccessibilityElement(accessibilityContainer: textView) linkElement.accessibilityLabel = "链接,\(url.absoluteString)" linkElement.accessibilityTraits = .link linkElement.accessibilityFrame = textView.convert(convertedRect, to: UIScreen.main.coordinateSpace) linkElement.accessibilityValue = url.absoluteString // 存储自定义信息用于点击时识别 linkElement.accessibilityUserInputLabels = [url.absoluteString] // 添加到无障碍元素数组 accessibilityElements.append(linkElement) }同时需要覆写
accessibilityActivate()方法处理 VoiceOver 下的双击激活逻辑:override func accessibilityActivate() -> Bool { guard let focusedElement = UIAccessibility.focusedElement(using: .notificationVoiceOver) as? UIAccessibilityElement, let urlStr = focusedElement.accessibilityValue, let url = URL(string: urlStr) else { return false } yourCustomHandleURLMethod(url) return true }方案3:如果使用自定义手势识别器,需要设置手势的委托避免拦截系统事件
给你添加的点击手势添加如下配置:yourTapGesture.cancelsTouchesInView = false yourTapGesture.delegate = self // 实现UIGestureRecognizerDelegate方法 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { // VoiceOver模式下不拦截手势,交给系统无障碍逻辑处理 if UIAccessibility.isVoiceOverRunning { return false } // 常规模式下判断点击位置是否是链接,是则先执行自定义逻辑再拦截 let tapLocation = touch.location(in: self) if let linkURL = getLinkURLAtLocation(tapLocation) { yourCustomHandleURLMethod(linkURL) return false } return true }
- 完成配置后打开 VoiceOver,单指滑动遍历文本区域,可听到系统播报「链接,xxx」的提示即说明识别成功
- 双击触发时可执行自定义逻辑即为适配完成
内容的提问来源于stack exchange,提问作者linto jacob

