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() } } }
关键改动说明
- 逻辑行号跟踪:
- 先将文本按
\n分割为逻辑行数组,遍历视觉行时,通过字符范围判断当前视觉行所属的逻辑行,仅当跨越逻辑行边界时才递增行号
- 先将文本按
- 末尾换行处理:
- 文本以
\n结尾时,逻辑行号为logicalLines.count + 1,匹配编辑器末尾换行新增空行的常规行为
- 文本以
- 空文本处理:保持显示行号1,符合编辑器默认交互逻辑
额外优化建议
- 可提前缓存逻辑行的长度数组,避免每次
draw时重复分割文本,提升渲染性能 - 在
text属性更新时(didSet方法)预计算逻辑行相关数据,减少绘制时的实时计算量
内容的提问来源于stack exchange,提问作者Stack Overflow
相关产品推荐
相关产品推荐

