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

两个问题:
当我不替换文本存储时,这很好。
这是我的密码:
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()
}
}发布于 2018-11-23 08:34:37
您已经在Swift中发现了一个bug (可能不仅仅是在Swift库中,也许是在一些更基本的方面)。
,怎么回事?
如果您创建一个编号列表而不是项目符号列表,您将能够更好地看到这一点。你不需要做任何复制和粘贴,只是:
您看到的是一个混乱,但您可以看到两个原始数字仍然在那里,新的中间列表项目,你开始点击返回是所有混乱的地方。
当您单击“返回”时,文本系统必须重新编号列表项,因为您刚刚插入了一个新项。首先,它执行这个“重命名”,即使它是一个项目符号列表,这就是为什么你看到你的例子中的混乱。其次,它通过从列表的开头重命名每个列表项并为刚刚创建的项插入一个新的数字来进行重命名。
在目标C中的过程
如果您将Swift代码转换为等效的目标-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中,以下内容取代了Swift:
override func replaceCharacters(in range: NSRange, with str: String)在Swift中的过程
在Objective中,字符串具有引用语义,可以更改字符串,代码的所有部分都要引用字符串,请参阅更改。在Swift中,字符串具有值语义,字符串在传递给函数时被复制(至少在名义上是这样);如果副本在被调用函数中被更改,调用方将不会在其副本中看到该更改。
文本系统是用(或为)Objective编写的,假定它可以利用引用语义是合理的。当您将其部分代码替换为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:] ()Frame #4是在返回时调用的目标C代码,在插入换行符之后,它调用内部例程_reformListAtIndex:,frame #3来进行重命名。这就调用了框架#2中的另一个Objective例程,它反过来调用了框架#1,它认为是Objective方法replaceCharactersInRange:withString:,但实际上是一个Swift替换。这个替换做了一些舞蹈转换对象-C引用语义字符串到Swift值语义字符串,然后调用框架#0,Swift replaceCharacters()。
跳舞很难
如果您通过Swift代码进行跟踪,就像您完成目标-C转换一样--当重命名到达将原始\t2)\t更改为\t3)\t的阶段时,您将看到一个错误步骤,给出的原始\t2)\t的范围是在前一步插入新的\t2)\t之前所给出的范围(即在前一步中插入了4个位置).结果是混乱的,然后再跳几步,代码会崩溃,字符串引用错误,因为索引都是错误的。
这表明,Objective代码依赖于引用语义,Swift舞蹈的编舞者将引用转换为值并返回到引用语义,但未能满足Objective代码的期望:因此,无论是目标-C代码,还是取代它的Swift代码,计算原始\t2)\t的范围时,它都是在字符串上进行的,而之前的插入新的\t2)\t并没有改变它的范围。
迷惑了?跳舞有时会让你头晕;-)
Fix?
在Objective中对您的NSTextStorage子类进行编码,然后转到bugreport.apple.com并报告错误。
HTH (比它更让你头晕)
https://stackoverflow.com/questions/53410817
复制相似问题