使用NSTextSelection表示插入点触发崩溃的技术咨询
TextKit 2 设置文本选择到文档末尾插入点时崩溃的问题
根据Apple官方文档,NSTextLayoutManager.textSelections的每个选定点代表插入点(即屏幕上的文本光标位置,可位于文档范围末尾)。但在非空文档中设置该属性时触发崩溃:例如文档内容为"T",尝试将插入点设置在第2列(非零索引,对应文档末尾),此时设置2..2的选择范围会导致TextKit计算位置偏移时因to字段不符合预期而崩溃。
请问:
textSelection的范围是否必须映射到文档有效范围?- 插入点是否需要单独追踪?
最小复现代码
CustomTextLocation.swift
import AppKit class CustomTextLocation: NSObject { var column: Int = 1 // 非零索引 init(column: Int) { self.column = column } override public var description: String { "\(column)" } } extension CustomTextLocation: NSTextLocation { func compare(_ location: NSTextLocation) -> ComparisonResult { guard let location = location as? CustomTextLocation else { fatalError("Expected CustomTextLocation") } if column < location.column { return .orderedAscending } else if column == location.column { return .orderedSame } else { return .orderedDescending } } override func isEqual(_ object: Any?) -> Bool { guard let location = object as? CustomTextLocation else { return false } return self.column == location.column } }
DocumentModel.swift
import AppKit class DocumentModel: NSTextContentManager { var layoutManager: NSTextLayoutManager! var data = "T" override var documentRange: NSTextRange { let start = CustomTextLocation(column: 1) let end = CustomTextLocation(column: data.utf8.count + 1) return NSTextRange(location: start, end: end)! } func attach(textLayoutManager: NSTextLayoutManager) { self.layoutManager = textLayoutManager super.addTextLayoutManager(layoutManager) } // MARK: NSTextContentManager 重写 override func textElements(for range: NSTextRange) -> [NSTextElement] { let textElement = NSTextElement(textContentManager: self) textElement.elementRange = documentRange return [textElement] } // MARK: NSTextElementProvider 重写 override func enumerateTextElements(from textLocation: NSTextLocation?, options: NSTextContentManager.EnumerationOptions = [], using block: (NSTextElement) -> Bool) -> NSTextLocation? { guard let from = textLocation as? CustomTextLocation else { return nil } let textElements = textElements(for: NSTextRange(location: from, end: documentRange.endLocation)!) for textElement in textElements { let _ = block(textElement) } return documentRange.endLocation } override func location(_ location: NSTextLocation, offsetBy offset: Int) -> NSTextLocation? { guard let location = location as? CustomTextLocation else { fatalError("Expected CustomTextLocation") } return CustomTextLocation(column: location.column + offset) } override func offset(from: NSTextLocation, to: NSTextLocation) -> Int { guard let from = from as? CustomTextLocation, let to = to as? CustomTextLocation else { fatalError("Expected CustomTextLocation") } print("offset from: \(String(describing: from)), to: \(String(describing: to))") return to.column - from.column } }
ViewController.swift
class ViewController: NSViewController { var layoutManager: NSTextLayoutManager! var textContainer: NSTextContainer! var documentModel: DocumentModel! override func viewDidLoad() { super.viewDidLoad() let textContainer = NSTextContainer(size: NSSize(width: 400, height: 400)) self.textContainer = textContainer let layoutManager = NSTextLayoutManager() layoutManager.textContainer = textContainer self.layoutManager = layoutManager self.documentModel = DocumentModel() documentModel.attach(textLayoutManager: layoutManager) layoutManager.enumerateTextLayoutFragments(from: CustomTextLocation(column: 1), options: [.reverse, .ensuresExtraLineFragment, .ensuresLayout]) { fragment in print("\(fragment)") return false } let selectionLocation = CustomTextLocation(column: 2) // 以下代码触发崩溃 layoutManager.textSelections = [NSTextSelection(selectionLocation, affinity: .downstream)] } }
崩溃信息
TextLocationCrash/DocumentModel.swift:60: Fatal error: Expected CustomTextLocation
问题解答
核心结论
textSelection范围必须落在文档有效范围内:包括文档末尾的插入点(即documentRange的endLocation),这个位置是合法的,不需要单独排除。- 插入点无需单独追踪:只要它是
documentRange内的合法NSTextLocation,TextKit就能正确处理。
崩溃原因分析
崩溃发生在offset(from:to:)方法中,因为TextKit内部处理选择逻辑时,可能会传入非CustomTextLocation的NSTextLocation实例(比如系统内置的NSNotFoundLocation),而你的代码直接通过fatalError终止程序,导致崩溃。
另外,你的enumerateTextElements方法在传入textLocation为nil时直接返回nil,不符合TextKit的预期——当from参数为nil时,应该从文档起始位置开始枚举文本元素。
修复方案
1. 完善offset(from:to:)方法的容错处理
不要直接触发fatalError,而是处理非CustomTextLocation的情况:
override func offset(from: NSTextLocation, to: NSTextLocation) -> Int { guard let fromLoc = from as? CustomTextLocation else { return 0 } if to == NSNotFoundLocation { // 处理系统内置的未找到位置,返回合理值 return 0 } guard let toLoc = to as? CustomTextLocation else { return 0 } print("offset from: \(fromLoc), to: \(toLoc)") return toLoc.column - fromLoc.column }
2. 修复enumerateTextElements的nil处理逻辑
当传入的textLocation为nil时,从文档起始位置开始枚举:
override func enumerateTextElements(from textLocation: NSTextLocation?, options: NSTextContentManager.EnumerationOptions = [], using block: (NSTextElement) -> Bool) -> NSTextLocation? { let startLocation: CustomTextLocation if let from = textLocation as? CustomTextLocation { startLocation = from } else { // 当传入nil时,从文档起始位置开始 startLocation = documentRange.startLocation as! CustomTextLocation } let textElements = textElements(for: NSTextRange(location: startLocation, end: documentRange.endLocation)!) for textElement in textElements { if block(textElement) { // 如果block返回true,提前终止枚举 return textElement.elementRange?.endLocation } } return documentRange.endLocation }
3. 完善CustomTextLocation的NSObject方法实现
重写hash方法,确保对象比较的一致性:
extension CustomTextLocation { override var hash: Int { column.hash } }
4. 可选:优化location(_:offsetBy:)的边界检查
确保偏移后的位置不会超出文档范围:
override func location(_ location: NSTextLocation, offsetBy offset: Int) -> NSTextLocation? { guard let location = location as? CustomTextLocation else { return nil } let newColumn = location.column + offset let docStart = (documentRange.startLocation as! CustomTextLocation).column let docEnd = (documentRange.endLocation as! CustomTextLocation).column // 确保位置在文档范围内 let clampedColumn = max(docStart, min(newColumn, docEnd)) return CustomTextLocation(column: clampedColumn) }
内容的提问来源于stack exchange,提问作者georgemp
相关产品推荐
相关产品推荐

