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

自定义NSTextContentManager时,recordEditAction的文本范围如何设置?

自定义NSTextContentManager时recordEditAction的范围问题解析

问题背景

我自定义了NSTextContentManager子类(不基于NSTextStorage/NSTextContentStorage,因为它们依赖NSAttributedString,我需要不同的存储方式)。编辑完成后尝试通过func recordEditAction(in originalTextRange: NSTextRange, newTextRange: NSTextRange)通知NSTextLayoutManager,但不清楚该传入什么样的范围。

例如现有文本“This”,将“s”替换为空字符串时,传入等效的(3,1)作为originalTextRange、(3,0)作为newTextRange,会导致NSTextLayoutManager异常(仍尝试布局(3,1)范围的文本)。

为逆向分析NSTextContentStorage的实现逻辑,我在Playground中编写了以下测试代码:

class CustomTextStorage: NSTextContentStorage {
    override func recordEditAction(in originalTextRange: NSTextRange, newTextRange: NSTextRange) {
        print("recordEditAction in \(originalTextRange) newTextRange \(newTextRange)")
        super.recordEditAction(in: originalTextRange, newTextRange: newTextRange)
    }

    override func processEditing(for textStorage: NSTextStorage, edited editMask: NSTextStorageEditActions, range newCharRange: NSRange, changeInLength delta: Int, invalidatedRange invalidatedCharRange: NSRange) {
        print("processEditing for \(textStorage) edited: \(editMask) range: \(newCharRange) changeInLength: \(delta) invalidatedRange: \(invalidatedCharRange)")
        super.processEditing(for: textStorage, edited: editMask, range: newCharRange, changeInLength: delta, invalidatedRange: invalidatedCharRange)
    }
}

let layoutManager = CustomTextLayoutManager()
let container = NSTextContainer(size: NSSize(width: 200, height: 200))
layoutManager.textContainer = container
let storage = CustomTextStorage()
storage.addTextLayoutManager(layoutManager)

storage.textStorage?.replaceCharacters(in: NSRange(location: 0, length: 0), with: "T")
storage.textStorage?.replaceCharacters(in: NSRange(location: 1, length: 0), with: "h")
storage.textStorage?.replaceCharacters(in: NSRange(location: 2, length: 0), with: "i")
storage.textStorage?.replaceCharacters(in: NSRange(location: 3, length: 0), with: "s")
storage.textStorage?.replaceCharacters(in: NSRange(location: 3, length: 1), with: "")

输出结果如下:

processEditing for T{
} edited: [.editedCharacters] range: {0, 1} changeInLength: 1 invalidatedRange: {0, 1}
recordEditAction in 0...0 newTextRange 0...1
processEditing for Th{
} edited: [.editedCharacters] range: {1, 1} changeInLength: 1 invalidatedRange: {1, 1}
recordEditAction in 0...0 newTextRange 0...1
processEditing for Thi{
} edited: [.editedCharacters] range: {2, 1} changeInLength: 1 invalidatedRange: {2, 1}
recordEditAction in 0...0 newTextRange 0...1
processEditing for This{
} edited: [.editedCharacters] range: {3, 1} changeInLength: 1 invalidatedRange: {3, 1}
recordEditAction in 0...0 newTextRange 0...1
processEditing for Thi{
} edited: [.editedCharacters] range: {3, 0} changeInLength: -1 invalidatedRange: {3, 0}
recordEditAction in 0...0 newTextRange 0...9223372036854775807

我疑惑:传入recordEditAction的范围与参数名指示不符,总是0...0和0...1;移除字符时newTextRange是类似Int.max的值。这些值的含义是什么?我应该传入什么?另外,processEditing属于NSTextStorageObserving而非NSTextContentManager的方法,它能获取正确的范围和编辑操作,推测它和recordEditAction配合让布局管理器知晓编辑情况。那在不使用NSTextStorage的自定义NSTextContentManager中,该如何实现类似功能?

关键疑问解析

特殊范围的含义

  • 0...0和0...1:这是NSTextContentStorage内部的适配逻辑导致的——它把NSTextStorage的字符编辑,统一映射为针对内容模型的增量锚点范围。0...0代表编辑发生前的空锚点范围,0...1代表编辑后新增的内容范围,本质是告诉布局管理器:内容在锚点位置发生了增量更新。
  • 0...9223372036854775807:这个值是Int.max,对应NSTextRange的内容末尾位置。当删除内容时,这个范围表示:原范围的内容已被移除,布局管理器需要重新计算从编辑位置到内容末尾的所有布局。

正确的传参逻辑

recordEditAction的参数不是基于原文本的字符范围,而是基于你自定义内容模型的位置偏移体系,需遵循以下规则:

  • 插入操作:
    • originalTextRange:插入位置的空范围(比如在位置3插入,就是3...3对应的NSTextRange)
    • newTextRange:插入后新增内容的范围(比如插入1个字符,就是3...4对应的NSTextRange)
  • 删除操作:
    • originalTextRange:被删除内容的完整范围(比如删除位置3的1个字符,就是3...4对应的NSTextRange)
    • newTextRange:删除后的空范围(即3...3对应的NSTextRange)
  • 替换操作:
    • originalTextRange:被替换内容的范围
    • newTextRange:替换后新增内容的范围

注意:这些范围必须和你在NSTextContentManager子类中实现的textRange(from:)、location(from:)等范围转换方法逻辑完全一致,确保布局管理器能正确解析。

无NSTextStorage时的实现方案

因为不依赖NSTextStorage,需要手动模拟processEditing的核心逻辑,配合recordEditAction通知布局管理器:

  1. 追踪编辑元数据:在自定义内容模型完成编辑后,记录三个关键信息:
    • 编辑发生的起始位置(基于你的内容模型偏移量)
    • 被删除的内容长度
    • 新增的内容长度
  2. 构造合规的NSTextRange:
    • 基于编辑前的位置和删除长度,构造originalTextRange
    • 基于编辑后的位置和新增长度,构造newTextRange
  3. 调用recordEditAction:将构造好的两个范围传入,触发布局管理器的更新逻辑
  4. 强制刷新布局:如果布局管理器未自动刷新,调用layoutManager.invalidateLayout(for:)传入受影响的范围,确保布局重新计算

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 10:05:57