You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS如何为NSMutableAttributedString子串添加可识别无障碍标识符

问题描述

尝试在UILabel的attributedText属性上配置NSMutableAttributedString,为属性字符串内的链接设置无障碍标识符。

实现目标

  • 为UILabel设置包含可点击跳转URL链接的NSAttributedString
  • 为链接添加可被Appium/VoiceOver识别的accessibilityIdentifier

当前现状

目前链接可正常显示,点击后也能正常跳转对应URL,但使用Accessibility Inspector查看时,整个内容会被识别为单个无障碍元素,无法单独识别链接子串。
当前使用的添加链接的扩展实现代码如下:

extension NSMutableAttributedString {
    @discardableResult
    public func setLink(for text: String,
                        linkURL: String) -> Bool {
        guard let url = URL(string: linkURL) else { return false }
        let linkRange = self.mutableString.range(of: text)

        guard linkRange.location != NSNotFound else { return false }

        self.addAttribute(.link,
                          value: url,
                          range: linkRange)

        // 该代码不生效
        self.addAttribute(.accessibilityTextCustom,
                          value: text,
                          range: linkRange)
        
        return true
    }
}

调用方式代码如下:

private lazy var attributedString: NSMutableAttributedString = {
        let string = "CNN and ESPN"
        let attributedString = NSMutableAttributedString(string: string)

        attributedString.setLink(for: "CNN", linkURL: "https://www.example.com")
        attributedString.setLink(for: "ESPN", linkURL: "https://www.example.com")

        return attributedString
    }()

    myLabel.attributedText = attributedString

官方文档中记载的accessibilityLink属性仅支持MacOS 10.4+系统,不兼容iOS。需要找到为NSMutableAttributedString的子串添加可在Appium/Accessibility Inspector中正常识别的无障碍标识符的可行方案,若直接添加属性的方案不可行,确认是否可通过无障碍容器实现需求。

解决方案

iOS系统下UILabel本身不支持将富文本子串拆分为独立无障碍元素,直接给NSMutableAttributedString添加无障碍属性的方案在UILabel上不会生效,目前有两种可落地的实现方案:

方案1:替换为UITextView实现(接入成本最低)

UILabel的无障碍实现会把整段内容合并为单个元素,而UITextView在满足isEditable = false、isSelectable = true的配置下,会自动将富文本中的链接识别为独立的无障碍元素,VoiceOver和Appium均可单独识别链接内容,无需额外配置复杂属性。
核心配置代码:

// 匹配UILabel显示效果
textView.isEditable = false
textView.isSelectable = true
textView.isScrollEnabled = false
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
// 直接赋值带.link属性的富文本即可
textView.attributedText = attributedString

如果需要给链接设置自定义accessibilityIdentifier,可以在视图渲染完成后遍历textView.accessibilityElements,匹配对应链接内容后修改标识即可。

方案2:自定义无障碍容器(完全可控,兼容UILabel)

如果业务场景必须使用UILabel,可以通过实现UIAccessibilityContainer协议手动拆分无障碍元素,步骤如下:

  • 提前记录所有链接的配置参数:在富文本中的range、显示文本、跳转地址、自定义无障碍标识
  • 重写UILabel(或承载UILabel的父视图)的accessibilityElements属性,通过TextKit计算每个链接在视图中的实际显示位置,为每个链接生成独立的UIAccessibilityElement实例,配置对应的accessibilityFrame、accessibilityLabel、accessibilityIdentifier,并将accessibilityTraits设置为.link
  • 非链接的普通文本可合并为一个独立的无障碍元素,保证读屏顺序符合预期

核心实现示例:

class LinkAccessibleLabel: UILabel {
    // 存储链接配置项
    var linkItems: [(range: NSRange, text: String, url: URL, accessibilityId: String)] = []
    
    override var accessibilityElements: [Any]? {
        get {
            guard let attributedText = attributedText, bounds.width > 0 else {
                return super.accessibilityElements
            }
            var elements: [UIAccessibilityElement] = []
            // 初始化TextKit组件计算文本布局
            let textStorage = NSTextStorage(attributedString: attributedText)
            let layoutManager = NSLayoutManager()
            textStorage.addLayoutManager(layoutManager)
            let textContainer = NSTextContainer(size: bounds.size)
            textContainer.lineFragmentPadding = 0
            textContainer.maximumNumberOfLines = numberOfLines
            textContainer.lineBreakMode = lineBreakMode
            layoutManager.addTextContainer(textContainer)
            
            // 为每个链接生成独立无障碍元素
            for item in linkItems {
                let linkElement = UIAccessibilityElement(accessibilityContainer: self)
                linkElement.accessibilityLabel = item.text
                linkElement.accessibilityIdentifier = item.accessibilityId
                linkElement.accessibilityTraits = .link
                // 计算链接实际显示的frame
                let glyphRange = layoutManager.glyphRange(forCharacterRange: item.range, actualCharacterRange: nil)
                var linkRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
                linkRect = linkRect.offsetBy(dx: textContainerInset.left, dy: textContainerInset.top)
                linkElement.accessibilityFrame = convert(linkRect, to: nil)
                elements.append(linkElement)
            }
            return elements
        }
        set {
            super.accessibilityElements = newValue
        }
    }
}

注意:计算链接位置时需要匹配Label的换行模式、最大行数、内边距等配置,保证accessibilityFrame和链接实际显示位置完全重合,避免VoiceOver焦点错位。


内容的提问来源于stack exchange,提问作者Adrian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 06:09:32