4

我有一个带有标准的基本 Mac 应用程序NSTextView。我正在尝试实现和使用 的子类NSTextStorage,但即使是非常基本的实现也会破坏列表编辑行为:

  1. 我添加了一个包含两个项目的项目符号列表
  2. 我将该列表进一步复制并粘贴到文档中
  3. 在粘贴的列表中按 Enter 会中断最后一个列表项的格式。

这是一个快速视频:

NSTextStorage 列表问题

两个问题:

  1. 粘贴列表的项目符号使用较小的字体大小
  2. 在第二个列表项后按 Enter 会破坏第三个项目

当我不替换文本存储时,这很好用。

这是我的代码:

ViewController.swift

@IBOutlet var textView:NSTextView!

override func viewDidLoad() {
   [...]
   textView.layoutManager?.replaceTextStorage(TestTextStorage())
}

TestTextStorage.swift

class TestTextStorage: NSTextStorage {

    let backingStore = NSMutableAttributedString()

    override var string: String {
        return backingStore.string
    }

    override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key:Any] {
        return backingStore.attributes(at: location, effectiveRange: range)
    }

    override func replaceCharacters(in range: NSRange, with str: String) {
        beginEditing()
        backingStore.replaceCharacters(in: range, with:str)
        edited(.editedCharacters, range: range,
               changeInLength: (str as NSString).length - range.length)
        endEditing()
    }

    override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
        beginEditing()
        backingStore.setAttributes(attrs, range: range)
        edited(.editedAttributes, range: range, changeInLength: 0)
        endEditing()
    }
}
4

1 回答 1

5

您在 Swift 中发现了一个错误(可能不只是在 Swift 库中,可能在一些更基本的东西中)。

那么发生了什么?

如果您创建一个编号列表而不是项目符号列表,您将能够更好地看到这一点。您无需进行任何复制和粘贴,只需:

  1. 输入“aa”,回车,输入“bb”
  2. 选择全部并格式化为编号列表
  3. 将光标放在“aa”的末尾并按回车...

您看到的是一团糟,但是您可以看到两个原始数字仍然存在,并且您通过按回车键开始的新中间列表项是所有混乱的地方。

当您点击返回时,文本系统必须重新编号列表项,因为您刚刚插入了一个新项目。首先,事实证明,即使它是一个项目符号列表,它也会执行这种“重新编号”,这就是为什么您在示例中看到混乱的原因。其次,它通过从列表的开头开始重新编号并重新编号每个列表项并为刚刚创建的项插入一个新编号来执行此重新编号。

Objective-C 中的过程

如果你将你的 Swift 代码翻译成等效的 Objective-C 并跟踪你可以观察到这个过程。从...开始:

1) aa
2) bb

内部缓冲区类似于:

\t1)\taa\n\t2)\tbb

首先插入返回:

\t1)\taa\n\n\t2)\tbb

然后_reformListAtIndex:调用内部例程并开始“重新编号”。首先它替换\t1)\t\t1)- 数字没有改变。然后它\t2)\t在两个新行之间插入,此时我们有:

\t1)\taa\n\t2)\t\n\t2)\tbb

然后它用给出替换原来\t2)\t\t3)\t

\t1)\taa\n\t2)\t\n\t3)\tbb

它的工作已经完成。所有这些替换都是基于指定要替换的字符范围,插入使用长度为0的范围,并经过:

- (void)replaceCharactersInRange:(NSRange)range withString:(NSString * _Nonnull)str

在 Swift 中被替换为:

override func replaceCharacters(in range: NSRange, with str: String)

Swift 中的过程

在 Objective-C 中,字符串具有引用语义,更改一个字符串,并且所有引用该字符串的代码部分都可以看到更改。在 Swift 中,字符串具有值语义,并且字符串在传递给函数等时会被复制(理论上至少是这样);如果副本在被调用函数中更改,则调用者将不会在其副本中看到该更改。

文本系统是用(或为)Objective-C 编写的,可以合理地假设它可以利用引用语义。当你用 Swift 替换它的部分代码时,Swift 代码必须做一些小动作,在列表重新编号阶段,当replaceCharacters()被调用时,堆栈将看起来像:

#0  0x0000000100003470 in SwiftTextStorage.replaceCharacters(in:with:)
#1  0x0000000100003a00 in @objc SwiftTextStorage.replaceCharacters(in:with:) ()
#2  0x00007fff2cdc30c7 in -[NSMutableAttributedString replaceCharactersInRange:withAttributedString:] ()
#3  0x00007fff28998c41 in -[NSTextView(NSKeyBindingCommands) _reformListAtIndex:] ()
#4  0x00007fff284fd555 in -[NSTextView(NSKeyBindingCommands) insertNewline:] ()

第 4 帧是当 return 被击中时调用的 Objective-C 代码,在插入换行符后它调用内部例程_reformListAtIndex:第 3 帧来进行重新编号。这在第 2 帧中调用了另一个 Objective-C 例程,该例程又调用第 1 帧,它认为是 Objective-C 方法replaceCharactersInRange:withString:,但实际上是 Swift 的替代品。这个替换将 Objective-C 引用语义字符串转换为 Swift 值语义字符串,然后调用帧 #0,Swift replaceCharacters()

跳舞很难

如果您在重新编号进入将原始代码更改为的阶段时跟踪您的 Swift 代码,就像您执行 Objective-C 翻译一样,\t2)\t\t3)\t将看到一个失误,为原始代码提供的范围\t2)\t是在插入新代码之前\t2)\t的范围上一步(即它偏离了 4 个位置)......你最终会变得一团糟,然后又跳了几个舞步,代码崩溃并出现字符串引用错误,因为索引都是错误的。

这表明 Objective-C 代码依赖于引用语义,而将引用转换为值并返回到引用语义的 Swift 舞蹈编排未能满足 Objective-C 代码的期望:所以要么当 Objective-C 代码,或者一些已经替换它的 Swift 代码,计算\t2)\t它正在执行的原始字符串的范围,该字符串没有被之前插入的 new 更改\t2)\t

使困惑?跳得好有时会让你头晕;-)

使固定?

NSTextStorage在 Objective-C 中编写你的子类bugreport.apple.com并报告错误。

HTH(不止让你头晕目眩)

于 2018-11-23T08:34:37.793 回答