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

TextKit2中UITextView代码块行间距统一问题求助

TextKit2 UITextView代码块行间距不一致问题解决

问题描述

使用TextKit2为UITextView实现代码块功能时,遇到换行后的行间距与无换行文本的行间距不一致的问题,期望两者保持一致(与无换行文本行间距相同)。尝试设置NSMutableParagraphStyle以及自定义NSTextLayoutFragment并重写相关属性均未解决。

现有代码问题分析

  1. 段落样式完全覆盖:在textContentStorage(_:textParagraphWith:)方法中,直接新建空白的NSMutableParagraphStyle替换原有样式,没有基于原文本的段落样式做针对性修改,导致代码块的行间距计算基准和普通文本脱节。
  2. 绘制偏移干扰布局:自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 10:55:23