Implementing NSSegmentedControl in NSToolbar to control NSTabViewController

994 Views Asked by At

In my macOS application, I'm trying to replicate the Photos.app implementation of NSSegmentedControl in NSToolbar to control an NSTabViewController. For reference, here's what that looks like:

Photos.app in macOS Sierra.

So, my approach was as follows:

  1. Hide the default NSTabView header using the Interface Builder
  2. Programmatically add an NSToolbar
  3. Insert NSSegmentedControl as an NSToolbarItem.
  4. Use a #selector to listen for changes to NSSegmentedControl.

Here's the current implementation:

class WindowController: NSWindowController, NSToolbarDelegate {

    // MARK: - Identifiers

    let mainToolbarIdentifier = NSToolbar.Identifier("MAIN_TOOLBAR")
    let segmentedControlIdentifier = NSToolbarItem.Identifier("MAIN_TABBAR")

    // MARK: - Properties

    var tabBar: NSSegmentedControl? = NSSegmentedControl(labels: ["One", "Two"], trackingMode: NSSegmentedControl.SwitchTracking.selectOne, target: self, action: #selector(didSwitchTabs))
    var toolbar: NSToolbar?
    var tabBarController: NSTabViewController?

    // MARK: - Life Cycle

    override func windowDidLoad() {
        super.windowDidLoad()

        self.toolbar = NSToolbar(identifier: mainToolbarIdentifier)
        self.toolbar?.allowsUserCustomization = false
        self.toolbar?.delegate = self

        self.tabBar?.setSelected(true, forSegment: 0)

        self.tabBarController = self.window?.contentViewController as? NSTabViewController
        self.tabBarController?.selectedTabViewItemIndex = 0

        self.window?.toolbar = self.toolbar
    }

    // MARK: - NSToolbarDelegate

    public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {

        var toolbarItem: NSToolbarItem

        switch itemIdentifier {
        case segmentedControlIdentifier:
            toolbarItem = NSToolbarItem(itemIdentifier: segmentedControlIdentifier)
            toolbarItem.view = self.tabBar
        case NSToolbarItem.Identifier.flexibleSpace:
            toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
        default:
            fatalError()
        }

        return toolbarItem
    }

    public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
    }

    public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
        return [NSToolbarItem.Identifier.flexibleSpace, segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
    }

    // MARK: - Selectors

    @objc func didSwitchTabs(sender: Any) {

        let segmentedControl = sender as! NSSegmentedControl

        if (segmentedControl.selectedSegment == 0) {
            self.tabBarController?.selectedTabViewItemIndex = 0
        } else if (segmentedControl.selectedSegment == 1) {
            self.tabBarController?.selectedTabViewItemIndex = 1
        }
    }

}

And, here it is in action:

Implementing NSSegmentedControl in NSToolbar to control NSTabViewController.

Now, I am new to macOS development and this feels like it's a very complicated and convoluted way of solving this problem. Is there an easier way I could achieve the same thing ? Perhaps somehow in Interface Builder ? What could be done to improve here ? What have I done wrong ?

Thanks for your time.

1

There are 1 best solutions below

0
Trung Phan On

enter image description hereFor anybody implementing NSSegmentedControl on the toolbar and it did not trigger IBAction, I got the same problem and pay my half-day to resolve this. The problem is I connect my segmented with the NSWindowController class.

To fix this, create a subclass of NSWindow, set that class to base class of window on your storyboard, then create @IBOutlet @IBAction link to NSWindow. Remember, link it with NSWindowController will not work.