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

使用NSAttributedString设置空格红色下划线时行尾空格无下划线问题

问题根因

iOS 富文本默认渲染规则下,当段落使用 .byWordWrapping 按单词换行时,会自动隐藏行尾空白字符的装饰属性(包括下划线、删除线、背景色等),仅保留字符的占位空间,所以会出现空格可选中、但下划线不显示的情况。

解决方案

方案1:空格后追加零宽不换行字符(优先推荐)

零宽不换行字符 \u{2060} 无视觉宽度也不会触发换行,添加在普通空格后,系统就不会将该空格判定为行尾可忽略的空白,下划线就能正常显示,不需要修改其他排版配置,对原文本的排版、字符统计几乎无影响。
仅需修改富文本生成逻辑即可:

let textView = UITextView()
let text = "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12"
let attributedText = NSMutableAttributedString()
for char in text {
    if char == " " {
        // 空格后追加零宽不换行字符
        let content = String(char) + "\u{2060}"
        attributedText.append(NSAttributedString(string: content, attributes: [
            .font: UIFont.systemFont(ofSize: 30, weight: .bold),
            .underlineColor: UIColor.red,
            .underlineStyle: NSUnderlineStyle.thick.rawValue
        ]))
    } else {
        attributedText.append(NSAttributedString(string: String(char), attributes: [
            .font: UIFont.systemFont(ofSize: 30, weight: .bold)
        ]))
    }
}

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 8
paragraphStyle.lineBreakMode = .byWordWrapping
attributedText.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedText.length))

textView.attributedText = attributedText
textView.isEditable = false
textView.textContainerInset = .zero

方案2:自定义NSLayoutManager强制绘制

如果不能修改原文本内容,可以自定义布局管理器,重写下划线绘制逻辑,主动识别行尾空格并补充绘制下划线:

// 自定义布局管理器
class UnderlineLayoutManager: NSLayoutManager {
    override func drawUnderline(forGlyphRange glyphRange: NSRange, underlineType underlineVal: NSUnderlineStyle, baselineOffset: CGFloat, lineFragmentRect lineRect: CGRect, lineFragmentGlyphRange lineGlyphRange: NSRange, containerOrigin: CGPoint) {
        super.drawUnderline(forGlyphRange: glyphRange, underlineType: underlineVal, baselineOffset: baselineOffset, lineFragmentRect: lineRect, lineFragmentGlyphRange: lineGlyphRange, containerOrigin: containerOrigin)
        
        // 检查当前glyph范围是否是行尾空格,手动绘制下划线
        let charRange = self.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
        guard let text = self.textStorage?.string as NSString? else { return }
        let char = text.character(at: charRange.location)
        if char == 32, NSLocationMaxRange(glyphRange) == NSLocationMaxRange(lineGlyphRange) {
            // 是行尾空格,获取位置手动绘制红色粗下划线
            let glyphRect = self.boundingRect(forGlyphRange: glyphRange, in: self.textContainers[0])
            let underlineRect = CGRect(x: glyphRect.minX + containerOrigin.x, y: glyphRect.maxY - 2 + containerOrigin.y, width: glyphRect.width, height: 2)
            UIColor.red.setFill()
            UIRectFill(underlineRect)
        }
    }
}

// 使用自定义布局管理器初始化UITextView
let textStorage = NSTextStorage()
let layoutManager = UnderlineLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: CGSize(width: 期望的textView宽度, height: CGFloat.greatestFiniteMagnitude))
layoutManager.addTextContainer(textContainer)
let textView = UITextView(frame: .zero, textContainer: textContainer)

// 后续富文本赋值、其他属性配置和原来一致即可

该方案完全不改动原文本内容,但需要额外处理绘制逻辑,适合不允许修改原始文本的场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 16:30:05