Programmatically Add Tabs to NSWindows without NSDocument

null

The Cocoa/AppKit documentation is very sparse when it comes to tabbing. You can make use of the native window tabbing introduced in macOS Sierra with a few simple method calls, though.

Conceptually, this is what you will need to do:

However, there are some caveats when implementing these methods naively. The plus button may stop working (no new tabs are added when you click it) and all default shortcuts are broken, their main menu items greyed out.

How to Implement newWindowForTab

First, where to add @IBAction override func newWindowForTab(_ sender: Any?)? That'll be the event handler to create new tabs.

We'll stick to NSWindowController for the rest of this post, no matter how you create it:

class WindowController: NSWindowController {
    @IBAction override func newWindowForTab(_ sender: Any?) {
        // Implementing this will display the button already
    }
}

How to Call addTabbedWindow

Once you have newWindowForTab(_:) in place, add functionality to it: create a new NSWindow and add it to the tab bar.

When you have the new window object, you can call addTabbedWindow. Using the Storyboard approach, for example, turns the implementation into this:

class WindowController: NSWindowController {
    @IBAction override func newWindowForTab(_ sender: Any?) {
        let newWindowController = self.storyboard!.instantiateInitialController() as! WindowController
        let newWindow = newWindowController.window!
        self.window!.addTabbedWindow(newWindow, ordered: .above)
    }
}

Fix the "+" Button and Main Menu

TL;DR: When you initialize a new window, set window.windowController = self to make sure the new tab forwards the responder chain messages just like the initial window.

Take into account how events are dispatched. Main Menu messages are sent down the responder chain, and so is newWindowForTab. NSApp.sendAction will fail for standard events if the source of the call doesn't connect up all the way – that means, at least up to your NSWindowController, maybe even up to your AppDelegate.

You have to make sure any additional window you add is, in fact, part of the same responder chain as the original window, or else the menu items will stop working (and be greyed-out/disabled). Similarly, the "+" button stops to work when you click on it.

If you forget to do this and run the code from above, it seems you can't create more than two tabs. That's the observation, but it's not an explanation. You can always create more tabs, but only from the original window/tab, not the new one; that's because the other tab is not responding to newWindowForTab.

Remember: "The other tab" itself is just an NSWindow. Your newWindowForTab implementation resides in the controller, though. That's up one level.

class WindowController: NSWindowController {
    @IBAction override func newWindowForTab(_ sender: Any?) {
        let newWindowController = self.storyboard!.instantiateInitialController() as! WindowController
        let newWindow = newWindowController.window!

        // Add this line:
        newWindow.windowController = self

        self.window!.addTabbedWindow(newWindow, ordered: .above)
    }
}

Now newWindow will have a nextResponder. This will fix message forwarding.

Using Multiple Window Controllers

The solution above shows how to add multiple windows of the same kind, reusing a single window controller for all of them.

You can move up newWindowForTab one level to another service object, say the AppDelegate. Then you could manage instances of NSWindowController instead of instances of NSWindow. I don't see why you would want to do that if you can share a single controller object.

I haven't tried to do anything fancy besides, but you should be able to use different windows and different window controllers and group them in the tab bar of a single host window. You will then need to keep the window controller instances around, too.

Browse the blog archive