I am developing a macOS Cocoa Application in Swift 5.1 . In the main window, I have two identical NSOutlineViews that have the same contents exactly. I would like to enable a sync mode, where if items are expanded/collapsed in one of the two NSOutlineViews, the corresponding item is expanded/collapsed in the other simultaneously. I tried to do this by implementing shouldExpandItem and shouldCollapseItem in the delegate. The delegate is the same object for both NSOutlineViews, and I have outlets that reference the two NSOutlineViews for distinguishing the two. The problem is, if I call expandItem programmatically in shouldExpandItem, the method is again called for the other NSOutlineView, leading to an infinite loop and a stack overflow. I have found a dirty solution that works, by momentarily setting the delegate of the relevant NSOutlineView to nil, expand/collapse the item, then set the delegate back. The code is the following:
func outlineView(_ outlineView: NSOutlineView, shouldExpandItem item: Any) -> Bool {
let filePath = (item as! FileSystemObject).fullPath
let trueItem = item as! FileSystemObject
trueItem.children = Array()
do {
let contents = try FileManager.default.contentsOfDirectory(atPath: filePath!)
for (_, item) in contents.enumerated() {
let entry = FileSystemObject.init()
entry.fullPath = URL.init(fileURLWithPath: filePath!).appendingPathComponent(item).path
if entry.exists {
trueItem.children.append(entry)
}
}
} catch {
}
if outlineView == self.leftOutlineView {
self.rightOutlineView.delegate = nil;
self.rightOutlineView.expandItem(item)
self.rightOutlineView.delegate = self;
} else {
self.leftOutlineView.delegate = nil;
self.leftOutlineView.expandItem(item)
self.leftOutlineView.delegate = self;
}
return true
}
func outlineView(_ outlineView: NSOutlineView, shouldCollapseItem item: Any) -> Bool {
if outlineView == self.leftOutlineView {
self.rightOutlineView.delegate = nil;
self.rightOutlineView.collapseItem(item)
self.rightOutlineView.delegate = self;
} else {
self.leftOutlineView.delegate = nil;
self.leftOutlineView.collapseItem(item)
self.leftOutlineView.delegate = self;
}
return true
}
It seems to work, but I am afraid that something could go wrong with this approach. Is setting the delegate momentarily to nil a possible solution, or should I be aware of any caveats ? is there another pattern that you can suggest to achieve this ? Thanks
EDIT: According to below comments and answers
I found the simplest solution thanks to the answers / comments I received. Instead of implementing the sync logic in the outlineViewShouldExpand/Collapse methods, it is possible to implement outlineViewDidExpand and outlineViewDidCollapse and place the sync logic there. the latter methods are not called when expanding/collapsing items programmatically, so there is no risk of infinite loop or stack overflow.
The code is as follows:
func outlineViewItemDidExpand(_ notification: Notification) {
let outlineView = notification.object as! NSOutlineView
let userInfo = notification.userInfo as! Dictionary<String, Any>
let item = userInfo["NSObject"]
if outlineView == self.leftOutlineView {
self.rightOutlineView.animator().expandItem(item)
} else {
self.leftOutlineView.animator().expandItem(item)
}
}
func outlineViewItemDidCollapse(_ notification: Notification) {
let outlineView = notification.object as! NSOutlineView
let userInfo = notification.userInfo as! Dictionary<String, Any>
let item = userInfo["NSObject"]
if outlineView == self.leftOutlineView {
self.rightOutlineView.animator().collapseItem(item)
} else {
self.leftOutlineView.animator().collapseItem(item)
}
}
Furthermore, now, I cannot understand why, the expansion / collapse of items is animated, which did not work with my original approach. I hope this can be helpful for somebody, it was very helpful to me. Thanks a lot.
outlineView(_:shouldExpandItem:)
is called before the item is expanded and calling expandItem()
back and forth causes an infite loop. outlineViewItemDidExpand(_:)
is called after the item is expanded and NSOutlineView.expandItem(_:)
does nothing when the item is already expanded (documented behaviour). When expanding the left outline view, expandItem()
of the right outline view does call outlineViewItemDidExpand(_:)
but the expandItem()
of the left outline view doensn't call outlineViewItemDidExpand(_:)
again.
func outlineViewItemDidExpand(_ notification: Notification) {
let outlineView = notification.object as! NSOutlineView
let item = notification.userInfo!["NSObject"]
if outlineView == self.leftOutlineView {
self.rightOutlineView.animator().expandItem(item)
} else {
self.leftOutlineView.animator().expandItem(item)
}
}