Swift中NSTreeController状态存储最佳方式:能否用NSCoding?
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’scontentarray) - 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
UUIDproperty to yourTreeNodeclass and use that for storing/retrieving expanded/selected nodes instead ofname. - Secure Coding: For macOS 10.13+, consider using
NSSecureCoding(update your class to conform to it, and setrequiringSecureCoding: truein the archiver/unarchiver) for better security. - Alternative Expansion Tracking: If you don’t want to store
isExpandedin your model, you can instead track expanded node paths (usingNSOutlineView’spath(forItem:)) and restore them withexpandItem(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

