Adding a New Tab Keyboard Shortcut To A SwiftUI MacOS Application

Justin Dickow
3 min readDec 29, 2020

--

You can find me on Twitter talking about product development and software engineering.

I got a MacBook Pro with Apple Silicon and I figured while I waited for Docker support and the like I’d try my hand at some iOS/MacOS development. I haven’t done much development in Apple’s ecosystem since Objective-C. (The last WWDC I bothered being around SF for was when Apple announced Swift in 2014). So in the last week I was surprised to find SwiftUI!

Long story short, I am really liking all of the free integration you get while developing a native MacOS application, but I’m finding a lot of things to be missing or requiring workarounds. For example, the WindowGroup you get out of the box to manage SwiftUI scenes is great. Running the default application, you’ll find things like support for File > New Window, and View > Show Tab Bar. Once you click Show Tab Bar, SwiftUI manages identical Scenes of the application with their own state across tabs. This is super useful. What I wanted to do was enabled the keyboard shortcut command-t to open a new tab similar to how there’s a command-n shortcut to open a new window. This turned out to not be as easy as it sounds (although I do have a simple, working answer at the very end of this post).

First, how it should work. Apple provides bindings between AppKit and SwiftUI with the likes of NSHostingController and NSHostingView. If your main application scene is a WindowGroup returning a ContentView like the default application boilerplate then you should be able to create a new ContentView, add it as the rootView of an NSHostingController, create a new window, and add the window to the tab group of the application’s current keyWindow. This would look like

let contentView = ContentView().environment(\.managedObjectContext, context)
let hostingController = NSHostingController(rootView: contentView)
let newWindow = NSWindow(contentViewController: hostingController)
NSApp.keyWindow?.addTabbedWindow(newWindow, ordered: .above)

If you try this, it will actually seem like it works if you have a particularly simple application. The problem is when you start to render things like toolbars within your application’s navigation that things break down. I found that this approach did not cause the sub views of contentView to render their toolbars properly. I tried plenty of workarounds here including rendering the views with toolbars twice, and other silly ideas.

So what does work? I observed two things. First, the default application ships with a shortcut for a new window, which creates a copy of the current window and does not have the same rendering issues, and second, when you show the tab bar of the application and click the plus button, a new tab is created that also doesn’t have the rendering issues. So there must be a way. First I tested creating a new window not inside a tab. This seemed to work fine and exactly the same as the provided new window shortcut. The follow code launches a new window that’s a copy of the current tab.

NSApp.keyWindow?.newWindowForTab(nil)

So the workaround involves using this properly managed window and adding it as a tab to our current window, which looks like

if let currentWindow = NSApp.keyWindow,
let windowController = currentWindow.windowController {
windowController.newWindowForTab(nil)
if let newWindow = NSApp.keyWindow,
currentWindow != newWindow {
currentWindow.addTabbedWindow(newWindow, ordered: .above)
}
}

Here we’re getting the current window if it exists, which is NSApp.keyWindow. This is appropriate in our circumstances because we’re responding to a keyboard shortcut or menu command, so the window that currently accepts keystrokes is the one we want. Then we’re copying the current tab of that window into a new window with windowController.newWindowForTab(nil). We then find the new window, which is now the NSApp.keyWindow and we add that window as a tab to our current window.

Finally, we want this command to be the well-known command-t keyboard shortcut for New Tab, under the existing File Command Menu. To accomplish this, we use a Command Group and specify the location for this command. The commands are only applied when the operating is MacOS. Here’s the entire body of the application with the command hoisted to the top for you to see.

var body: some Scene {
let mainWindow = WindowGroup {
ContentView()
.environment(\.managedObjectContext, context)
}
#if os(macOS)
mainWindow.commands {
CommandGroup(after: .newItem) {
Button(action: {
if let currentWindow = NSApp.keyWindow,
let windowController = currentWindow.windowController {
windowController.newWindowForTab(nil)
if let newWindow = NSApp.keyWindow,
currentWindow != newWindow {
currentWindow.addTabbedWindow(newWindow, ordered: .above)
}
}
}) {
Text("New Tab")
}
.keyboardShortcut("t", modifiers: [.command])
}
}
#else
mainWindow
#endif
}

Not so bad considering you then get everything else that comes with a tabbable (sp?) application for free! Don’t forget to find me on Twitter

--

--