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

UITextView自动换行时行号无法识别的技术求助

解决自动换行时行号计数错误的问题

你的核心问题是把视觉上的自动换行当成了独立的逻辑行,enumerateLineFragments会遍历所有视觉行(包括文本超长自动折行的),直接每次自增lineNum会导致行号重复。我们需要基于文本中的换行符\n统计逻辑行号,只在遇到真正的换行时才增加行号。

修改方案

主要修改LineNumberedTextView的draw方法,调整逻辑行号的跟踪逻辑:

class LineNumberedTextView: UITextView {
    let lineNumberGutterWidth: CGFloat = 40
    let lineNumberPadding: CGFloat = 5
    
    var currentLines: [Int] = []
    
    override func draw(_ rect: CGRect) {
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.alignment = .right
        
        let attributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.monospacedSystemFont(ofSize: 15, weight: .ultraLight),
            .paragraphStyle: paragraphStyle,
            .foregroundColor: UIColor(white: 0.8, alpha: 1)
        ]
        let attributes_active: [NSAttributedString.Key: Any] = [
            .font: UIFont.monospacedSystemFont(ofSize: 15, weight: .ultraLight),
            .paragraphStyle: paragraphStyle,
            .foregroundColor: UIColor.black
        ]
        
        // 初始化逻辑行号,空文本时显示1
        var currentLogicalLine = 1
        // 获取所有逻辑行(按\n分割)
        let logicalLines = textStorage.string.components(separatedBy: .newlines)
        // 记录当前处理到的逻辑行索引
        var logicalLineIndex = 0
        // 记录累计字符长度(包含换行符)
        var cumulativeLength = 0
        
        var lineRect = CGRect.zero
        
        layoutManager.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: textStorage.length)) { (rect, usedRect, textContainer, glyphRange, stop) in
            // 将字形范围转换为文本字符范围
            let characterRange = self.layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
            
            // 检查当前视觉行是否跨越逻辑行边界
            while logicalLineIndex < logicalLines.count {
                let lineLength = logicalLines[logicalLineIndex].count + 1 // +1对应换行符长度
                if cumulativeLength + lineLength > characterRange.location {
                    break
                }
                cumulativeLength += lineLength
                currentLogicalLine += 1
                logicalLineIndex += 1
            }
            
            // 绘制当前逻辑行号
            lineRect = CGRect(x: 5, y: rect.origin.y + self.textContainerInset.top, width: self.lineNumberGutterWidth - self.lineNumberPadding, height: rect.height)
            let lineNumStr = "\(currentLogicalLine)"
            let attributedString = NSAttributedString(string: lineNumStr, attributes: self.currentLines.contains(currentLogicalLine) ? attributes_active : attributes)
            attributedString.draw(in: lineRect)
        }
        
        // 处理空文本情况
        if textStorage.string.isEmpty {
            let attributedString = NSAttributedString(string: "1", attributes: currentLines.contains(1) ? attributes_active : attributes)
            attributedString.draw(at: CGPoint(x: lineNumberGutterWidth - lineNumberPadding - 5, y: textContainerInset.top))
            return
        }
        
        // 处理末尾带换行符的情况:新增一行逻辑行号
        if textStorage.string.hasSuffix("\n") {
            let rect = lineRect.offsetBy(dx: 0, dy: lineRect.height)
            let newLineNum = logicalLines.count + 1
            let attributedString = NSAttributedString(string: "\(newLineNum)", attributes: currentLines.contains(newLineNum) ? attributes_active : attributes)
            attributedString.draw(in: rect)
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        textContainerInset = UIEdgeInsets(top: textContainerInset.top, left: lineNumberGutterWidth, bottom: textContainerInset.bottom, right: textContainerInset.right)
        setNeedsDisplay()
    }
    
    override var text: String! {
        didSet {
            setNeedsDisplay()
        }
    }
}

关键改动说明

  1. 逻辑行号跟踪:
    • 先将文本按\n分割为逻辑行数组,遍历视觉行时,通过字符范围判断当前视觉行所属的逻辑行,仅当跨越逻辑行边界时才递增行号
  2. 末尾换行处理:
    • 文本以\n结尾时,逻辑行号为logicalLines.count + 1,匹配编辑器末尾换行新增空行的常规行为
  3. 空文本处理:保持显示行号1,符合编辑器默认交互逻辑

额外优化建议

  • 可提前缓存逻辑行的长度数组,避免每次draw时重复分割文本,提升渲染性能
  • 在text属性更新时(didSet方法)预计算逻辑行相关数据,减少绘制时的实时计算量

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 05:45:47