为什么具有 autosaveExpandedItems true 的基于视图的 NSOutlineView 忽略在 reloadData 时扩展?

Why View-Based NSOutlineView with autosaveExpandedItems true ignores expanded upon reloadData?

我使用自动保存展开状态的 NSOutlineView。如果我在数据源更新时手动重新加载数据,则不再调用 func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? 数据源方法并且每个单元格都会崩溃。知道为什么会发生这种情况吗?

尝试使用 nil send 作为参数重新加载 Item,但仍然不行。

我用它来持久化展开的行:

func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
    return NSKeyedArchiver.archivedData(withRootObject: item)
}

func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
    guard let data = object as? Data,
        let item = NSKeyedUnarchiver.unarchiveObject(with: data) as? Category else { return nil }
    let foundItem = recursiveSearch(for: item, in: viewModel.dataSource.value)
    return foundItem
}

然后重新加载数据:

  viewModel.dataSource.subscribe(onNext: { [weak self] _ in
            self?.outlineView.reloadData()
  }).disposed(by: disposeBag)

恕我直言,自动保存是一种 half-baked 功能,它无法按预期工作。换句话说,它的实现方式是在您的应用程序启动时(仅一次)恢复状态,然后您就可以自己了。

利用 outlineViewItemDidExpand(_:) & outlineViewItemDidCollapse(_:) 实现您自己的一个(尤其是当我们重新加载时,...)。

如果您不想实现自定义自动保存,可以使用一些技巧。但我不会依赖他们。

第一个技巧 - 告诉 NSOutlineView 重新加载持久状态

NSOutlineView 继承自 NSTableViewautosaveName 属性 文档说:

If you change the value of this property to a new name, the table reads in any saved information and sets the order and width of this table view’s columns to match. Setting the name to nil removes any previously stored state from the user defaults.

这里有什么不准确的地方 - 将其设置为 nil 不会删除之前为 NSOutlineView 存储的展开项目状态。我们可以用它来强制 NSOutlineView 重新加载展开的项目状态:

class ViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource {
    @IBOutlet var outlineView: NSOutlineView!
    // It's for testing, to demonstrate the persistent state reloading
    private var doNotLoad = true

    override func viewDidAppear() {
        super.viewDidAppear()
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            self.doNotLoad = false
            
            let autosaveName = self.outlineView.autosaveName
            self.outlineView.autosaveName = nil            
            self.outlineView.reloadData()
            self.outlineView.autosaveName = autosaveName
        }
    }

    func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
        if (doNotLoad) {
            return 0
        }
        return item == nil ? data.count : (item as! Node).children.count
    }
}

如果您想遵守文档,请不要使用 nil 并设置一些假名。但我希望一旦错误被修复,如果我们更改 autosaveName 或将其设置为 nil.

,持久状态将被删除

第二招——加载和扩展自己

假设您有以下 Node class:

class Node {
    let id: Int
    let children: [Node]
    // ...
}

并且您的数据源实现了:

func outlineView(_ outlineView: NSOutlineView, persistentObjectForItem item: Any?) -> Any? {
    (item as! Node).id
}

func outlineView(_ outlineView: NSOutlineView, itemForPersistentObject object: Any) -> Any? {
    guard let id = object as? Int else { return nil }
    return data.firstNode { [=12=].id == id }
}

firstNode与这个问题无关,但是这里是实现(因为在代码中提到了):

extension Array where Self.Element == Node {
    // Search for a node (recursively) until a matching element is found
    func firstNode(where predicate: (Element) throws -> Bool) rethrows -> Element? {
        for element in self {
            if try predicate(element) {
                return element
            }
            if let matched = try element.children.firstNode(where: predicate) {
                return matched
            }
        }
        return nil
    }
}

然后你可以reloadData & 自己展开所有项目:

outlineView.reloadData()
if outlineView.autosaveExpandedItems,
   let autosaveName = outlineView.autosaveName,
   let persistentObjects = UserDefaults.standard.array(forKey: "NSOutlineView Items \(autosaveName)"),
   let itemIds = persistentObjects as? [Int] {
    
    itemIds.forEach {
        let item = outlineView.dataSource?.outlineView?(self.outlineView, itemForPersistentObject: [=14=])
        self.outlineView.expandItem(item)
    }
}