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
相关产品推荐
相关产品推荐

