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

iOS Swift中为span标签文本添加圆角背景的实现问题

问题根源

你当前代码的核心错误是范围基准不匹配:你基于带<span>标签的原始字符串计算字符范围,但UITextView显示的是移除标签后的干净字符串——原始字符串里的位置包含了标签字符,干净字符串里这些字符已被删除,导致用原始范围查找干净字符串的文本位置时出现偏移,背景自然错位。

修正方案

下面是完整的修正代码,核心改进点包括:

  • 提前提取所有需要高亮的文本内容
  • 在干净字符串中准确定位高亮范围
  • 处理多行文本的背景渲染
  • 确保UITextView布局完成后再添加背景
class ViewController: UIViewController {
    let string = "LLorem Ipsum is simply <span>dummy</span> text of the printing and <span>typesetting industry</span>. Lorem Ipsum has been the industry's <span>standard dummy</span> text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has <span>survived not</span> only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of <span>Lorem Ipsum.</span>"

    @IBOutlet weak var textView: UITextView!
    let paragraphStyle = NSMutableParagraphStyle()
    // 存储需要高亮的文本内容
    private var highlightedTexts: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        textView.isEditable = false
        textView.isSelectable = true

        // 1. 提取所有<span>包裹的文本内容
        let pattern = "<span>([^<]+)</span>"
        let regex = try! NSRegularExpression(pattern: pattern, options: [])
        let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count))
        highlightedTexts = matches.map {
            (string as NSString).substring(with: $0.range(at: 1))
        }

        // 2. 移除标签并设置富文本
        let cleanedString = removeSpanTags(from: string)
        let attributedString = NSMutableAttributedString(string: cleanedString)
        paragraphStyle.lineHeightMultiple = 1.25
        attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedString.length))
        textView.attributedText = attributedString

        // 3. 延迟执行,确保UITextView完成布局后再计算文本位置
        DispatchQueue.main.async { [weak self] in
            self?.addHighlightBackgrounds()
        }
    }

    // 添加圆角背景视图
    private func addHighlightBackgrounds() {
        guard let textView = textView else { return }
        let nsCleanedString = textView.attributedText.string as NSString
        var currentLocation = 0

        // 先移除旧的背景视图,避免重复添加
        textView.subviews.filter { $0.tag == 1001 }.forEach { $0.removeFromSuperview() }

        for text in highlightedTexts {
            // 在干净字符串中按顺序查找文本范围,避免重复匹配
            let range = nsCleanedString.range(of: text, options: [], range: NSRange(location: currentLocation, length: nsCleanedString.length - currentLocation))
            if range.location != NSNotFound {
                if let frames = framesOfText(in: range, in: textView) {
                    for frame in frames {
                        let bgView = UIView(frame: frame)
                        bgView.tag = 1001 // 标记视图,方便后续移除
                        bgView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5)
                        bgView.layer.cornerRadius = 5.0
                        bgView.layer.borderWidth = 1.0
                        bgView.layer.borderColor = UIColor.blue.cgColor
                        textView.addSubview(bgView)
                        // 把背景放在文本下方
                        textView.sendSubviewToBack(bgView)
                    }
                }
                // 更新起始位置,查找下一个文本
                currentLocation = range.location + range.length
            }
        }
    }

    // 移除<span>标签
    func removeSpanTags(from htmlString: String) -> String {
        let pattern = "<span>([^<]+)</span>"
        let regex = try! NSRegularExpression(pattern: pattern, options: [])
        let range = NSRange(location: 0, length: htmlString.utf16.count)
        return regex.stringByReplacingMatches(in: htmlString, options: [], range: range, withTemplate: "$1")
    }

    // 获取文本所有行的frame(支持多行文本)
    func framesOfText(in range: NSRange, in textView: UITextView) -> [CGRect]? {
        guard let start = textView.position(from: textView.beginningOfDocument, offset: range.location),
              let end = textView.position(from: start, offset: range.length),
              let textRange = textView.textRange(from: start, to: end) else {
            return nil
        }

        // 获取文本所有行的选择rect
        let selectionRects = textView.selectionRects(for: textRange)
        return selectionRects.compactMap { rect in
            // 过滤无效的空rect
            rect.rect.isEmpty ? nil : textView.convert(rect.rect, from: textView.textInputView)
        }
    }
}
关键改进说明
  1. 范围基准统一:以移除标签后的干净字符串为基准,查找高亮文本的位置,确保范围完全匹配。
  2. 多行文本支持:用selectionRects(for:)替代firstRect(for:),获取文本所有行的frame,解决换行时背景不完整的问题。
  3. 布局时机修正:通过DispatchQueue.main.async延迟执行背景添加逻辑,确保UITextView完成布局后再计算文本位置,避免frame不准确。
  4. 避免重复添加:给背景视图设置标签,每次添加前移除旧视图,防止重复叠加。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 09:50:55