我有一个带有标准NSTextView
的基本Mac应用程序。我正在尝试实现和使用NSTextStorage
的子类,但是即使是非常基本的实现也无法实现列表编辑行为:
我添加了两个项目符号列表
我将该列表进一步复制并粘贴到文档中
在粘贴的列表中按Enter键将中断最后一个列表项的格式。
这是一个快速视频:
两个问题:
粘贴列表的项目符号点使用较小的字体
在第二个列表项中断第三个项目后按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()
}
}
最佳答案
您已经在Swift中找到了一个bug(也许不仅是Swift库中的bug,还可能是更基本的东西)。
那么发生了什么?
如果您创建一个编号列表而不是项目符号列表,您将能够更好地看到这一点。您无需执行任何复制和粘贴操作,只需:
输入“ aa”,点击返回,输入“ bb”
请全选并格式化为编号列表
将光标放在“ 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()
。跳舞很难
如果在重新编号进入将原始
\t2)\t
更改为\t3)\t
的阶段时,就像在Objective-C转换中一样浏览Swift代码,您会发现步伐不正确,原始\t2)\t
的范围是在上一步中插入新\t2)\t
之前是什么状态(即,它偏离了4个位置)...并且您最终陷入一团糟,之后又跳了几个舞步,代码崩溃,并出现字符串错误,错误为索引都是错误的。这表明,Objective-C代码依赖于引用语义,而Swift Dance的编舞者将引用转换为值并转换回引用语义却未能达到Objective-C代码的期望:因此,无论是当Objective-C代码,或一些替换了它的Swift代码,它计算的是原始字符串
\t2)\t
的范围,该字符串在之前未插入新\t2)\t
的情况下未更改。困惑?好的舞蹈有时会让你头晕;-)
固定?
在Objective-C中为
NSTextStorage
的子类编码,然后转到bugreport.apple.com
并报告错误。HTH(不仅仅是让您晕眩)
关于cocoa - 子类化NSTextStorage中断列表编辑,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53410817/