You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

启用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 02:10:39