非可选NSTextLocation参数传入Null?NSTextElementProvider问题求助
关于NSTextElementProvider的offset方法参数异常问题
问题背景
NSTextElementProvider包含如下可选方法:
optional func offset( from: NSTextLocation, to: NSTextLocation ) -> Int
其中to参数为非可选类型,NSTextLocation是协议,按预期应传入实现该协议的实例,但实际从AppKit收到了类似Null的(BuiltIn.RawPointer) 0x0值。
目前使用以下代码做防护:
var end: MarkupText.Location if let to = to as? MarkupText.Location { end = to } else { end = range.upperBound }
需要解决两个问题:
- 为何编译器允许这种情况发生?
- 是否有更优的处理或规避方式?
补充说明:通过自定义NSTextLocation和NSTextLayoutManager可复现问题——为NSTextLayoutManager设置NSTextContainer时会触发崩溃,不设置则不会。相关示例代码如下:
ViewController.swift
// // ViewController.swift // TextLocationCrash // // Created by George Philip Malayil on 06/10/23. // import Cocoa class ViewController: NSViewController { var layoutManager: NSTextLayoutManager! var textContainer: NSTextContainer! var documentModel: DocumentModel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. let textContainer = NSTextContainer(size: NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)) 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, .ensuresLayout]) { fragment in print("\(fragment)") return false } } }
Document.swift
// // Document.swift // TextLocationCrash // // Created by George Philip Malayil on 06/10/23. // import AppKit class CustomTextLocation: NSObject { var column: Int = 1 // Not zero indexed. 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 Document.Location") } if column < location.column { return .orderedAscending } else if column == location.column { return .orderedSame } else { return .orderedDescending } } }
DocumentModel.swift
// // DocumentModel.swift // TextLocationCrash // // Created by George Philip Malayil on 06/10/23. // import AppKit class DocumentModel: NSTextContentManager { var layoutManager: NSTextLayoutManager! var data = "" override var documentRange: NSTextRange { NSTextRange(location: CustomTextLocation(column: 1), end: CustomTextLocation(column: 1))! } func attach(textLayoutManager: NSTextLayoutManager) { self.layoutManager = textLayoutManager super.addTextLayoutManager(layoutManager) } // MARK: NSTextContentManager overrides override func textElements(for range: NSTextRange) -> [NSTextElement] { [NSTextParagraph(textContentManager: self)] } // MARK: NSTextElementProvider Overrides 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 offset(from: NSTextLocation, to: NSTextLocation) -> Int { guard let from = from as? CustomTextLocation, let to = to as? CustomTextLocation else { fatalError("Expected CustomTextLocation") } return to.column - from.column } }
问题解答
1. 编译器为何允许这种情况?
核心原因是Swift与Objective-C的互操作特性:
NSTextElementProvider是Objective-C协议,而Objective-C中所有对象指针都允许为nil,没有严格的非可选引用类型。- Swift导入Objective-C协议时,会将协议类型参数默认标记为非可选,但Objective-C代码仍能绕过Swift编译检查传入
nil。 - AppKit内部的Objective-C代码在某些边界场景(比如布局初始化未完成时)可能传入无效指针,导致Swift收到类似Null的值。
2. 更优的处理/规避方式
方式一:简化安全防护逻辑
用guard let替代分支判断,直接处理非法参数,避免崩溃:
override func offset(from: NSTextLocation, to: NSTextLocation) -> Int { guard let fromLoc = from as? CustomTextLocation, let toLoc = to as? CustomTextLocation else { // 可选方案:返回0或使用文档范围默认偏移 return 0 } return toLoc.column - fromLoc.column }
如果需要用range.upperBound作为 fallback,确保range是当前有效的文本范围即可。
方式二:修复documentRange初始化问题
示例代码中documentRange返回空范围且使用强制解包!,是触发崩溃的直接诱因之一。建议:
- 确保
documentRange返回有效非空范围(根据实际内容调整) - 避免强制解包,改用安全初始化:
override var documentRange: NSTextRange { let start = CustomTextLocation(column: 1) let end = CustomTextLocation(column: data.count + 1) // 按实际内容长度调整 return NSTextRange(location: start, end: end) ?? NSTextRange(location: start, end: start)! }
方式三:避免fatalError,优雅处理无效位置
在CustomTextLocation的compare方法中,不要直接崩溃,而是返回合理的比较结果:
func compare(_ location: NSTextLocation) -> ComparisonResult { guard let location = location as? CustomTextLocation else { // 假设无效位置视为文档末尾 return .orderedAscending } // 原有比较逻辑 if column < location.column { return .orderedAscending } else if column == location.column { return .orderedSame } else { return .orderedDescending } }
方式四:延迟绑定textContainer
从复现条件看,布局初始化阶段的异常导致崩溃,可延迟设置textContainer到文档模型完全初始化后:
override func viewDidLoad() { super.viewDidLoad() let layoutManager = NSTextLayoutManager() self.layoutManager = layoutManager self.documentModel = DocumentModel() documentModel.attach(textLayoutManager: layoutManager) // 延迟设置textContainer,避免初始化阶段异常 DispatchQueue.main.async { let textContainer = NSTextContainer(size: NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)) self.textContainer = textContainer layoutManager.textContainer = textContainer layoutManager.enumerateTextLayoutFragments(from: CustomTextLocation(column: 1), options: [.reverse, .ensuresLayout]) { fragment in print("\(fragment)") return false } } }
内容的提问来源于stack exchange,提问作者georgemp
相关产品推荐
相关产品推荐

