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

SwiftUI中NSTextView每10字符加间隔的排除路径异常排查

问题:NSTextView排除路径间隔位置异常(每7字符而非10字符)

我在SwiftUI开发macOS应用,通过NSViewRepresentable集成NSTextView功能正常。想给文本视图每10个字符后添加间隔,文本采用等宽字体且仅保留字母,不想通过添加空格修改数据模型,因此尝试用排除路径实现,但实际间隔出现在每7个字符后,同时设置了左侧较宽边距,相关代码如下:

struct TestView: View {
    var string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum"
        
    var body: some View {
        MyTextView(text: string)
    }
}

struct MyTextView: NSViewRepresentable {
    var text: String

    final class Coordinator: NSObject, NSTextViewDelegate {
        var parent: MyTextView
        unowned var textView: NSTextView!
        init(parent: MyTextView) {
            self.parent = parent
        }

        func textDidChange(_ notification: Notification) {
            self.parent.text = textView.string
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeNSView(context: Context) -> NSTextView {
        let t = NSTextView()
        context.coordinator.textView = t
        t.delegate = context.coordinator
        return t
    }

    func updateNSView(_ t: NSTextView, context: Context) {
        t.textStorage?.setAttributedString(NSAttributedString(string: text
            .filter { $0.isLetter } // only keep letters
            .uppercased()) // make text uppercase
        )
        t.font = NSFont.monospacedSystemFont(ofSize: 12.0, weight: NSFont.Weight.regular)
        t.textContainer?.exclusionPaths = exclusionPaths(t)
    }
    
    func exclusionPaths(_ textView: NSTextView) -> [NSBezierPath] {
        var exclusionPaths = [NSBezierPath]()

        let leftMargin = CGRect(x: 0, y: 0, width: 30, height: CGFloat.greatestFiniteMagnitude)
        exclusionPaths.append(NSBezierPath(rect: leftMargin))

        guard let tc = textView.textContainer, let lm = tc.layoutManager else { return exclusionPaths }
        
        var currentX = 0.0
        let gapWidth = 10.0
        var index = 0

        repeat {
            let glyphRect = lm.boundingRect(forGlyphRange: NSMakeRange(index, 10), in: tc) // get bounding rect for 10 glyphs/characters
            currentX = leftMargin.width + glyphRect.origin.x
            
            let columnRect = CGRect(x: currentX, y: 0, width: gapWidth, height: CGFloat.greatestFiniteMagnitude)
            exclusionPaths.append(NSBezierPath(rect: columnRect))
            
            index += 10 // skip 10 characters
        }
        while currentX < 500 // this should be the width of the textView

        return exclusionPaths
    }
}

问题原因分析

  • 字形范围与字符范围混淆:boundingRect(forGlyphRange:in:)方法要求传入字形索引范围,但代码中直接传入了字符索引范围。虽然等宽字体下字符和字形通常一一对应,但布局管理器计算时会受文本容器边距、换行等影响,导致坐标计算偏差。
  • 字体与文本设置顺序错误:先设置文本再设置字体,导致文本存储未正确应用字体属性,字形宽度计算不准确。
  • 硬编码终止条件:用固定值500作为宽度判断,未匹配文本视图实际可用宽度,导致间隔位置计算错误。

修复方案

修改updateNSView方法(调整字体与文本顺序)

先设置字体,再创建带字体属性的富文本,确保布局管理器基于正确字体计算:

func updateNSView(_ t: NSTextView, context: Context) {
    // 先设置字体,再应用到文本上
    t.font = NSFont.monospacedSystemFont(ofSize: 12.0, weight: .regular)
    let processedText = text
        .filter { $0.isLetter }
        .uppercased()
    // 给文本明确绑定字体属性
    t.textStorage?.setAttributedString(NSAttributedString(string: processedText, attributes: [.font: t.font!]))
    t.textContainer?.exclusionPaths = exclusionPaths(t)
}

重写exclusionPaths方法(基于等宽特性直接计算)

利用等宽字体单个字符宽度固定的特性,直接计算间隔位置,避免依赖字形范围的错误:

func exclusionPaths(_ textView: NSTextView) -> [NSBezierPath] {
    var exclusionPaths = [NSBezierPath]()

    let leftMargin: CGFloat = 30
    // 左侧边距排除路径
    let leftMarginRect = CGRect(x: 0, y: 0, width: leftMargin, height: .greatestFiniteMagnitude)
    exclusionPaths.append(NSBezierPath(rect: leftMarginRect))

    guard let font = textView.font, let _ = textView.textContainer else { return exclusionPaths }
    
    // 获取等宽字体单个字符的宽度
    let charWidth = font.advancement(forGlyph: 0).width
    let charsPerGroup: CGFloat = 10
    let gapWidth: CGFloat = 10
    // 文本视图的可用宽度(总宽度减去左侧边距)
    let availableWidth = textView.bounds.width - leftMargin
    
    // 每组10字符的总宽度
    let groupWidth = charWidth * charsPerGroup
    // 第一个间隔的起始位置:左侧边距 + 第一组字符宽度
    var currentX = leftMargin + groupWidth
    
    // 循环添加间隔,直到超出可用宽度
    while currentX < availableWidth {
        let gapRect = CGRect(x: currentX, y: 0, width: gapWidth, height: .greatestFiniteMagnitude)
        exclusionPaths.append(NSBezierPath(rect: gapRect))
        // 下一个间隔位置:当前位置 + 组宽度 + 间隔宽度
        currentX += groupWidth + gapWidth
    }

    return exclusionPaths
}

修复要点说明

  1. 字体优先绑定:确保文本应用正确的字体属性,让布局管理器能准确计算字符宽度。
  2. 直接计算字符宽度:等宽字体下无需依赖字形范围,直接通过字体API获取单个字符宽度,计算更精准。
  3. 适配实际视图宽度:用文本视图的实际宽度替代硬编码值,适配不同尺寸的视图。
  4. 简化间隔逻辑:基于组宽度和间隔宽度依次计算位置,避免字符索引带来的偏差。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 23:39:59