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

Swift中NSTreeController状态存储最佳方式:能否用NSCoding?

Saving NSTreeController State with NSCoding (No Core Data Needed)

Absolutely—using NSCoding (or its secure counterpart NSSecureCoding) is a fantastic lightweight alternative to Core Data for saving your NSTreeController state, especially since you’re finding Core Data overkill for this use case. Let’s break down exactly how to pull this off:

1. Make Your Tree Node Model Conform to NSCoding

The core of your NSTreeController is its content array—so first, your custom tree node class needs to support encoding/decoding. This lets you save the entire hierarchy of nodes, plus any state properties (like expansion status if you want to store it directly in the model).

Here’s a Swift example of a node class implementing NSCoding:

class TreeNode: NSObject, NSCoding {
    var name: String
    var children: [TreeNode]
    var isExpanded: Bool // Optional: Store expansion state directly in the model

    init(name: String, children: [TreeNode] = []) {
        self.name = name
        self.children = children
        self.isExpanded = false
    }

    // Encode all properties for storage
    func encode(with coder: NSCoder) {
        coder.encode(name, forKey: "nodeName")
        coder.encode(children, forKey: "nodeChildren")
        coder.encode(isExpanded, forKey: "nodeIsExpanded")
    }

    // Decode properties when loading
    required init?(coder: NSCoder) {
        guard let decodedName = coder.decodeObject(forKey: "nodeName") as? String else {
            return nil
        }
        self.name = decodedName
        self.children = coder.decodeObject(forKey: "nodeChildren") as? [TreeNode] ?? []
        self.isExpanded = coder.decodeBool(forKey: "nodeIsExpanded")
    }
}

2. Save the Full Tree State

When you need to persist the state (e.g., on window close, or when the user triggers a save), you’ll want to save three key things:

  • The root nodes of the tree (the NSTreeController’s content array)
  • The list of expanded nodes (from the NSOutlineView)
  • The selected nodes (from the NSTreeController)

Here’s how to package and save this data to a plist file:

func saveTreeState() {
    // Get the root nodes from the tree controller
    guard let rootNodes = treeController.content as? [TreeNode] else { return }

    // Capture expanded nodes (store their unique identifiers—use UUIDs if names aren't unique)
    let expandedNodes = outlineView.expandedItems as? [TreeNode] ?? []
    let expandedIDs = expandedNodes.map { $0.name }

    // Capture selected nodes
    let selectedNodes = treeController.selectedObjects as? [TreeNode] ?? []
    let selectedIDs = selectedNodes.map { $0.name }

    // Package everything into a dictionary for easy encoding
    let stateDictionary: [String: Any] = [
        "rootNodes": rootNodes,
        "expandedNodeIDs": expandedIDs,
        "selectedNodeIDs": selectedIDs
    ]

    // Save to a plist in the user's Documents directory
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let stateFileURL = documentsURL.appendingPathComponent("TreeState.plist")

    do {
        let archivedData = try NSKeyedArchiver.archivedData(
            withRootObject: stateDictionary,
            requiringSecureCoding: false
        )
        try archivedData.write(to: stateFileURL)
    } catch {
        print("Failed to save tree state: \(error.localizedDescription)")
    }
}

3. Restore the Tree State on Launch

To bring back the exact state when your app starts, read the plist, decode the data, and reconfigure the NSTreeController and NSOutlineView:

func loadTreeState() {
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
    let stateFileURL = documentsURL.appendingPathComponent("TreeState.plist")

    guard FileManager.default.fileExists(atPath: stateFileURL.path) else { return }

    do {
        let archivedData = try Data(contentsOf: stateFileURL)
        guard let stateDictionary = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(archivedData) as? [String: Any] else {
            return
        }

        // Restore the root nodes
        if let restoredRootNodes = stateDictionary["rootNodes"] as? [TreeNode] {
            treeController.content = restoredRootNodes
        }

        // Restore expanded nodes
        if let expandedIDs = stateDictionary["expandedNodeIDs"] as? [String] {
            let nodesToExpand = expandedIDs.compactMap { id in
                findNode(withName: id, in: treeController.content as? [TreeNode] ?? [])
            }
            outlineView.expandItems(nodesToExpand)
        }

        // Restore selected nodes
        if let selectedIDs = stateDictionary["selectedNodeIDs"] as? [String] {
            let nodesToSelect = selectedIDs.compactMap { id in
                findNode(withName: id, in: treeController.content as? [TreeNode] ?? [])
            }
            treeController.setSelectedObjects(nodesToSelect)
        }

    } catch {
        print("Failed to load tree state: \(error.localizedDescription)")
    }
}

// Helper: Recursively find a node by its identifier
private func findNode(withName name: String, in nodes: [TreeNode]) -> TreeNode? {
    for node in nodes {
        if node.name == name {
            return node
        }
        if let matchingChild = findNode(withName: name, in: node.children) {
            return matchingChild
        }
    }
    return nil
}

Key Notes for Reliability

  • Use Unique Identifiers: If your node names aren’t guaranteed to be unique, add a UUID property to your TreeNode class and use that for storing/retrieving expanded/selected nodes instead of name.
  • Secure Coding: For macOS 10.13+, consider using NSSecureCoding (update your class to conform to it, and set requiringSecureCoding: true in the archiver/unarchiver) for better security.
  • Alternative Expansion Tracking: If you don’t want to store isExpanded in your model, you can instead track expanded node paths (using NSOutlineView’s path(forItem:)) and restore them with expandItem(at:)—but unique identifiers are more robust if the tree structure changes between saves.

This approach is way lighter than Core Data, avoids the overhead of managing a persistent store, and gives you full control over exactly what state you save and how.

内容的提问来源于stack exchange,提问作者Defari

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 08:55:30