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

TextKit 2 NSTextView编辑时产生重复文本段落与片段问题

TextKit 2 NSTextView修改已有段落时重复生成段落/片段的解决思路

问题场景

在NSTextView子类的keyDown方法中操作textStorage,修改已生成NSTextParagraph和NSTextFragment的文本区域(比如在段落末尾插入新内容)时,即便所有操作都包裹在textContentStorage.performEditingTransaction闭包中,仍会出现重复的段落和片段。尝试过三种操作方式均无效:

  • 在段落末尾插入与前文属性匹配的新属性字符串
  • 用包含新文本的属性字符串替换原段落文本
  • 先删除原段落文本,再插入包含新文本的完整属性字符串

示例代码:

let editLocation = editRange.location
guard editLocation > 0 else {
    break
}
let attrs = textStorage.attributes(at: editLocation, effectiveRange: nil)

// paragraphStartLocation是自定义的NSAttributedString扩展方法
guard let paragraphStartLoc = textStorage.paragraphStartLocation(from: editLocation) else {
    assertionFailure(); return
}

var paragraphRange = NSRange(location: paragraphStartLoc, length: editLocation - paragraphStartLoc + 1)
var fullParagraph = textStorage.attributedSubstring(from: paragraphRange).string
fullParagraph += newText

let newAttrStr = NSAttributedString(string: fullParagraph, attributes: attrs)

textContentStorage.performEditingTransaction {
    textStorage.deleteCharacters(in: paragraphRange)
    textStorage.insert(newAttrStr, at: paragraphStartLoc)
}

问题核心原因

问题出在NSTextContentStorageDelegate的textContentStorage(_:range:)方法调用逻辑上:即使在事务中执行了「删除原段落+插入新段落」的操作,TextKit 2仍会分两次调用代理——第一次针对原段落的删除范围,第二次针对新增文本的插入范围。如果代理对这两个范围都返回完整的段落对象,就会导致旧段落未被正确清理,新段落又被创建,最终UI出现重复元素。

可行解决方案

方案1:优化代理逻辑,避免重复返回段落对象

在代理方法中加入范围判断,只针对完整段落范围返回段落对象,对部分范围直接返回空数组,让TextKit 2自行处理:

func textContentStorage(_ textContentStorage: NSTextContentStorage, didRequestTextElementsFor range: NSRange) -> [NSTextElement] {
    let string = textContentStorage.string as NSString
    // 获取当前请求范围对应的完整段落范围
    let fullParagraphRange = string.paragraphRange(for: range)
    
    // 仅当请求范围是完整段落时,才返回段落对象
    if NSEqualRanges(range, fullParagraphRange) {
        return [NSTextParagraph(range: fullParagraphRange)]
    } else {
        // 部分范围返回空,避免重复创建段落
        return []
    }
}

方案2:子类化NSTextContentStorage接管段落识别逻辑

可以通过子类化NSTextContentStorage,重写段落识别逻辑,确保只对完整段落创建NSTextParagraph对象:

class CustomTextContentStorage: NSTextContentStorage {
    override func textElements(for range: NSRange) -> [NSTextElement] {
        var elements = [NSTextElement]()
        let string = self.string as NSString
        
        // 枚举范围内所有完整段落,只处理完整段落边界
        var currentLoc = range.location
        while currentLoc < range.location + range.length {
            let paraRange = string.paragraphRange(for: NSRange(location: currentLoc, length: 0))
            // 可加入缓存逻辑,复用已存在的段落对象
            elements.append(NSTextParagraph(range: paraRange))
            currentLoc = paraRange.location + paraRange.length
        }
        return elements
    }
}

额外优化建议

  • 尽量使用单次替换操作替代「先删后插」:直接用replaceCharacters(in:with:)修改原段落范围,能让TextKit 2更准确识别到是同一段落的修改,减少拆分的范围请求次数:
    textContentStorage.performEditingTransaction {
        textStorage.replaceCharacters(in: paragraphRange, with: newAttrStr)
    }
    
  • 所有文本修改必须严格放在performEditingTransaction闭包内,避免事务外的操作导致TextKit 2状态混乱。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 15:15:18