Finder and Notes have a peculiar behaviour that I am seeking to reproduce. The ‘flexible space’ in the NSToolbar seems to take the dimensions of the split view into account. For instance, the first group of buttons aligns on the left side with the right side of the sidebar. The second group of icons aligns with the right side of the first column. When I widen the sidebar, the toolbar items move along with it.
Is it possible to reproduce this?
Solution
With the solution provided by @KenThomases, I have implemented this as follows:
final class MainWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
window?.toolbar?.delegate = self
// Make sure that tracking is enabled when the toolbar is completed
DispatchQueue.main.async {
self.trackSplitViewForFirstFlexibleToolbarItem()
}
}
}
extension MainWindowController: NSToolbarDelegate {
func toolbarWillAddItem(_ notification: Notification) {
// Make sure that tracking is evaluated only after the item was added
DispatchQueue.main.async {
self.trackSplitViewForFirstFlexibleToolbarItem()
}
}
func toolbarDidRemoveItem(_ notification: Notification) {
trackSplitViewForFirstFlexibleToolbarItem()
}
/// - Warning: This is a private Apple method and may break in the future.
func toolbarDidReorderItem(_ notification: Notification) {
trackSplitViewForFirstFlexibleToolbarItem()
}
/// - Warning: This method uses private Apple methods that may break in the future.
fileprivate func trackSplitViewForFirstFlexibleToolbarItem() {
guard var toolbarItems = self.window?.toolbar?.items, let splitView = (contentViewController as? NSSplitViewController)?.splitView else {
return
}
// Add tracking to the first flexible space and remove it from the group
if let firstFlexibleToolbarItem = toolbarItems.first, firstFlexibleToolbarItem.itemIdentifier == NSToolbarFlexibleSpaceItemIdentifier {
_ = firstFlexibleToolbarItem.perform(Selector(("setTrackedSplitView:")), with: splitView)
toolbarItems.removeFirst()
}
// Remove tracking from other flexible spaces
for flexibleToolbarItem in toolbarItems.filter({ $0.itemIdentifier == NSToolbarFlexibleSpaceItemIdentifier }) {
_ = flexibleToolbarItem.perform(Selector(("setTrackedSplitView:")), with: nil)
}
}
}
You can do this with Apple-private methods, although that's not allowed in the App Store.
There's a private method,
-setTrackedSplitView:
, onNSToolbarItem
. It takes anNSSplitView*
as its parameter. You need to call it on the flexible-space toolbar item that you want to track a split view and pass it the split view it should track. To protect yourself against Apple removing the method, you should check ifNSToolbarItem
responds to the method before trying to use it.Since the user can customize and re-order the toolbar, you generally need to enumerate the window's toolbar's items. For the first one whose identifier is
NSToolbarFlexibleSpaceItemIdentifier
, you set the split view it should track. For all other flexible-space items, you clear (set tonil
) the split view to track. You need to do that when the window is first set up and again in the toolbar delegate's-toolbarWillAddItem:
and-toolbarDidRemoveItem:
methods. There's also another undocumented delegate method,-toolbarDidReorderItem:
, where I've found it useful to update the toolbar.