TextKit2中UITextView代码块行间距统一问题求助
TextKit2 UITextView代码块行间距不一致问题解决
问题描述
使用TextKit2为UITextView实现代码块功能时,遇到换行后的行间距与无换行文本的行间距不一致的问题,期望两者保持一致(与无换行文本行间距相同)。尝试设置NSMutableParagraphStyle以及自定义NSTextLayoutFragment并重写相关属性均未解决。
现有代码问题分析
- 段落样式完全覆盖:在
textContentStorage(_:textParagraphWith:)方法中,直接新建空白的NSMutableParagraphStyle替换原有样式,没有基于原文本的段落样式做针对性修改,导致代码块的行间距计算基准和普通文本脱节。 - 绘制偏移干扰布局:自定义
RTECodeLayoutFragment的draw方法中手动偏移绘制点,但未同步调整布局的padding属性,造成视觉上行间距的错位感知;同时margin设置可能干扰段落内部的行间距计算逻辑。
解决方案
1. 基于原有段落样式调整代码块行间距
修改段落样式的生成逻辑,保留原文本的基础样式,仅调整代码块需要的行间距属性:
extension RichTextEditorTextView: NSTextContentStorageDelegate { func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { guard range.lowerBound > 0 else { return nil } let paragraphText = textContentStorage.textStorage!.attributedSubstring(from: range) guard paragraphText.attribute(.codeId, at: 0, effectiveRange: nil) != nil else { return nil } // 获取原文本的段落样式,无则用默认样式 let originalStyle = paragraphText.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle ?? NSParagraphStyle.default let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.setParagraphStyle(originalStyle) // 仅修改行间距相关属性,保持其他样式与原文本一致 paragraphStyle.lineSpacing = 0 // 此处设置为普通文本的行间距值 paragraphStyle.paragraphSpacing = 0 paragraphStyle.paragraphSpacingBefore = 0 let textWithDisplayAttributes = NSMutableAttributedString(attributedString: paragraphText) textWithDisplayAttributes.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: textWithDisplayAttributes.length)) return NSTextParagraph(attributedString: textWithDisplayAttributes) } }
2. 修正自定义布局片段的布局逻辑
通过重写padding属性替代手动绘制偏移,让布局管理器正确计算文本位置:
class RTECodeLayoutFragment: NSTextLayoutFragment { private let _topMargin: CGFloat init(textElement: NSTextElement, range rangeInElement: NSTextRange?, topMargin: CGFloat) { _topMargin = topMargin super.init(textElement: textElement, range: rangeInElement) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var renderingSurfaceBounds: CGRect { let containerWidth = textLayoutManager?.usageBoundsForTextContainer.width ?? layoutFragmentFrame.width return CGRect(x: 0, y: 0, width: containerWidth, height: layoutFragmentFrame.height) } override var topMargin: CGFloat { _topMargin } override var bottomMargin: CGFloat { 10 } // 通过重写padding属性实现左侧缩进,替代绘制时的手动偏移 override var leadingPadding: CGFloat { 10 } override var trailingPadding: CGFloat { 10 } override func draw(at point: CGPoint, in context: CGContext) { context.saveGState() // 基于布局边界绘制背景,避免margin干扰 let borderRect = CGRect( x: 0, y: 0, width: renderingSurfaceBounds.width, height: renderingSurfaceBounds.height + topMargin + bottomMargin ) context.addRect(borderRect) context.setFillColor(Colors.midnight.cgColor) context.drawPath(using: .fill) context.restoreGState() // 直接使用原始绘制点,不再手动偏移 super.draw(at: point, in: context) } }
关键说明
- 代码块的段落样式必须基于原文本样式修改,确保行间距的计算基准和普通文本一致,避免完全替换样式导致的排版差异。
- 自定义布局片段的内边距应通过重写
leadingPadding等属性实现,让布局管理器参与计算,避免手动绘制偏移造成的视觉错位。
内容的提问来源于stack exchange,提问作者Adam Bardon
相关产品推荐
相关产品推荐

