启用usesAutomaticRowHeights的NSOutlineView出现重入警告及异常求助
关于NSTableView委托重入操作警告的问题解析
警告含义
WARNING: Application performed a reentrant operation in its NSTableView delegate. 这个警告表示:在NSOutlineView(继承自NSTableView)的委托/数据源方法执行过程中,应用触发了会再次调用委托方法的操作,也就是出现了重入。这种重入会破坏视图布局计算的正常流程,直接导致行重叠、点击无响应等UI异常,尤其在启用自动行高时,布局计算对委托方法的调用顺序稳定性要求更高。
问题触发原因
你的场景里,核心诱因是初始化流程中调用时机不当:
- 在
loadView里,调用outlineView.reloadData()后立刻执行expandItem(data)展开所有节点。当启用自动行高+实现isGroupItem方法时,展开操作会触发NSOutlineView重新计算行高,而这个计算过程会再次调用委托/数据源方法,和当前未完成的初始化流程形成重入。 - 即便
isGroupItem返回false,只要实现了这个方法,NSOutlineView在自动行高计算时会额外执行分组相关的逻辑检查,大幅提升了重入的概率。
解决方案
方案1:延迟展开操作
把节点展开操作延迟到视图初始化和布局完成后执行,避免在委托/数据源的执行周期内触发重入。可以用主队列异步调用,或者移到viewDidAppear方法中:
方式A:主队列延迟调用
修改loadView中的代码:
outlineView.reloadData() // 延迟到主队列下一轮循环执行,避开初始化时的委托调用周期 DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.expandItem(self.data) }
方式B:移到viewDidAppear
override func loadView() { outlineView = NSOutlineView(dataSource: self, delegate: self, columns: [ NSTableColumn(identifier: "", title: "", width: 400), NSTableColumn(identifier: "", title: "", width: 100) ]) let scrollView = NSScrollView(documentView: outlineView) view = NSView() view.addSubview(scrollView) NSLayoutConstraint.constrain(scrollView, toMarginsOf: view, edgeInsets: .zero) outlineView.reloadData() } override func viewDidAppear() { super.viewDidAppear() expandItem(data) }
方案2:移除不必要的isGroupItem实现
如果你的场景不需要分组功能,直接删除outlineView(_:isGroupItem:)方法即可。NSOutlineView默认会将所有项视为非分组项,这样能跳过分组相关的额外布局检查,从根源减少重入可能。
完整修正代码
import Cocoa class ViewController: NSViewController, NSOutlineViewDataSource, NSOutlineViewDelegate { var outlineView: NSOutlineView! let data: Any = [[[[[[[0], [0, [0,0,0]]], [0, [0, [0,0]],0,0,0,0,0]]], [[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]],1]]]] override func loadView() { outlineView = NSOutlineView(dataSource: self, delegate: self, columns: [ NSTableColumn(identifier: "", title: "", width: 400), NSTableColumn(identifier: "", title: "", width: 100) ]) let scrollView = NSScrollView(documentView: outlineView) view = NSView() view.addSubview(scrollView) NSLayoutConstraint.constrain(scrollView, toMarginsOf: view, edgeInsets: .zero) outlineView.reloadData() // 延迟展开操作 DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.expandItem(self.data) } } private func expandItem(_ item: Any) { if let item = item as? [Any] { outlineView.expandItem(item) for content in item { expandItem(content) } } } func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool { return item is [Any] } func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int { return ((item ?? data) as! [Any]).count } func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any { return ((item ?? data) as! [Any])[index] } func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { let view = NSTableCellView() return view } } // 以下扩展代码保持不变 extension NSOutlineView { convenience init(dataSource: NSOutlineViewDataSource, delegate: NSOutlineViewDelegate, columns: [NSTableColumn]) { self.init() self.dataSource = dataSource self.delegate = delegate for column in columns { addTableColumn(column) } usesAutomaticRowHeights = true } } extension NSTableColumn { convenience init(identifier: String, title: String, width: Double, minWidth: Double = 10, maxWidth: Double = .infinity) { self.init(identifier: NSUserInterfaceItemIdentifier(identifier)) self.title = title self.width = width self.minWidth = minWidth self.maxWidth = maxWidth } } extension NSLayoutConstraint { static func constrain(_ view: NSView, toMarginsOf superview: NSView, edgeInsets: NSEdgeInsets) { NSLayoutConstraint.activate([ NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: superview, attribute: .top, multiplier: 1, constant: edgeInsets.top), NSLayoutConstraint(item: view, attribute: .left, relatedBy: .equal, toItem: superview, attribute: .left, multiplier: 1, constant: edgeInsets.left), NSLayoutConstraint(item: superview, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: edgeInsets.bottom), NSLayoutConstraint(item: superview, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: edgeInsets.right) ]) } } extension NSEdgeInsets { static var zero: NSEdgeInsets { return NSEdgeInsets(all: 0) } init(all: Double) { self.init(top: all, left: all, bottom: all, right: all) } } extension NSScrollView { convenience init(documentView: NSView) { self.init() self.documentView = documentView hasVerticalScroller = true translatesAutoresizingMaskIntoConstraints = false } }
内容的提问来源于stack exchange,提问作者Nickkk
相关产品推荐
相关产品推荐

