Background
I'm working with Xcode 12.5.1 (12E507) and macOS 11.5.1. I am trying to show a tree of NSManagedObject instances in an NSOutlineView. Here's the general schema:
The root of the tree I'm showing (object nil in the NSOutlineView) is always a Layer. Its list of rules can contain a mix of Rule objects and Section objects (thus the PolicyItem entity which Rules and Sections inherit from). Section objects can contain further Rule objects. Rules may contain a reference to another Layer, but generally don't.
The canonical data is on a remote server accessed via an API. The server does not allow cycles (so a Layer contains a Section contains a Rule, but that Rule cannot reference the Layer it is under), so no need to account for that.
I'm trying to handle rearranging rules. I have the drag-and-drop part working in the UI, and I am able to make the API call to rearrange the rule. The rule gets properly moved in the Core Data store, but on a background thread.
Layer.rules can be an ordered relationship (with ordering managed by Core Data), or I can manage the ordering myself with an integer to track the position of each PolicyItem. Same for Section.rules.
My Question
How should I recognize the changes in the data store and update the UI?
-
For unordered things, NSFetchedResultsController is pretty great. I've hooked it up to NSOutlineView.insertItems, .removeItems, and so on. I'm not sure how to get it to work with ordered data, though.
Maybe use manual ordering, add a relationship from each rule to its parent layer (regardless of intervening section), run the FRC to fetch rules whose parent layer is the layer I want, then sort by [Rule.inRulesOfSection?.positionInt? ?? 0, Rule.positionInt]? That seems really *****, and I don't think it would handle changes in a layer in a rule.
-
With the upcoming async/await, I could redo the API interaction and update the positions in my drop handler when the call returns. That only helps for drag-and-drop, though. If somebody else makes a change on the server while the client is disconnected, it wouldn't be reflected on the client until the client's view is reloaded.
-
Is this something KVO should be able to do? I tried setting it up, but had trouble adding observers to any of the Sections.
-
Should I instead focus on NSManagedObjectContext change notifications? It looks like those notifications only contain information about the new state, not about the transition from old to new. That is, if I move a Rule from one Section to another, it looks like I would get the Rule and both Sections in the NSUpdatedObjectsKey, but it wouldn't tell me the rule was removed from one and added to the other. Or if it was rearranged, I would see the new position, but I don't see how I would find the old position.
Is there some other option I've missed?
Yes, there was an option I missed. NSTreeController seems to handle this really well, with a few small quirks.
- I had to add a 'rules' property to my rule. Simple extension:
extension Rule {
@objc public var rules: NSSet? {
return self.passToLayer?.rules
}
}
- Setting up the binding was a little weird, as I've never done that in code before. Wound up with roughly this:
@IBOutlet var ruleViewContainer:NSView!
var ruleOutlineView:NSOutlineView = NSOutlineView()
private var layerToShow:Layer
private var treeController:NSTreeController = NSTreeController()
...
override func viewWillAppear() {
...
let outlineContainer = NSScrollView(frame:ruleViewContainer.bounds)
outlineContainer.autoresizingMask = [.width, .height]
outlineContainer.documentView = ruleOutlineView
outlineContainer.hasVerticalScroller = true
outlineContainer.hasHorizontalScroller = true
ruleViewContainer.addSubview(outlineContainer)
ruleOutlineView.usesAlternatingRowBackgroundColors = true
ruleOutlineView.usesAutomaticRowHeights = true
ruleOutlineView.delegate = self
ruleOutlineView.dataSource = self
ruleOutlineView.doubleAction = #selector(self.doubleClick)
ruleOutlineView.target = self
treeController.childrenKeyPath = "rules"
treeController.bind(NSBindingName.content, to: layerToShow, withKeyPath: "rules", options: nil)
ruleOutlineView.bind(NSBindingName.content, to: treeController, withKeyPath: "arrangedObjects", options: nil)
}
- Now all the items in the NSOutlineView are of type NSTreeNode. This broke my existing drag-and-drop and
outlineView(_ outlineView:, viewFor tableColumn:, item:)
. Easy enough to fix. Just wrap all the references toitem
like this:
let representedItem = (item as! NSTreeNode).representedObject
Then use representedItem
just like you used item
before.
I handle the updates in my API interaction object, which then updates my Core Data store if the server confirms the attempted change was successful. The UI then updates.
I still have a few weird glitches. Moved objects are sometimes out of order. That should be much easier to figure out, though.