Toggle Sidebar in SwiftUI NavigationView on macOS

Viewed 5374

I have a macOS Application with a NavigationView and want to have the default ToggleSidebar item in the toolbar of the window.

Currently I set the target of the ToolbarItem to the AppDelegate in toolbarWillAddItem(_) of the NSToolbarDelegate.

Inside of the AppDelegate I implemented

@objc func toggleSidebar(_ sender: Any) {
    ((window.contentView?.subviews.first?.subviews.first?.subviews.first as? NSSplitView)?.delegate as? NSSplitViewController)?.toggleSidebar(self)
}

This solution is working right now. If the implementation of SwiftUI will change this breaks.

So how can this be done in a better way?

4 Answers

Since macOS Big Sur beta 4 you can add default sidebar commands with SwiftUI 2.0.

var body: some Scene {
    WindowGroup {
        NavigationView {
            Group {
                SidebarView()
                ContentView()
            }
        }
    }
    .commands {
        SidebarCommands()
    }
}

This code will add the "Toggle Sidebar" shortcut:

enter image description here

SidebarView code:

var body: some View {
    List {
        ForEach(0..<5) { index in
            Text("\(index)")
        }
    }
    .listStyle(SidebarListStyle())
}

In SwiftUI 2.0 you can use NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) with a toolbar button like this:

.toolbar {
    ToolbarItem(placement: .navigation) {
        #if os(macOS)
        Button(action: toggleSidebar, label: {
            Image(systemName: "sidebar.left")
        })
        #endif
    }
}

func toggleSidebar() {
    #if os(macOS)
    NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
    #endif
}

While you could try to perform #selector(NSSplitViewController.toggleSidebar(_:)) on keyWindow?.contentViewController or keyWindow?.firstResponder?, it appears that this doesn't work consistently in some situations.

Instead, you can use this command:

NSApp.sendAction(#selector(NSSplitViewController.toggleSidebar(_:)), to: nil, from: nil)

It sends the toggleSidebar selector to the first object that can react to it, meaning, the only sidebar in the current window. This behavior is better documented on Apple's documentation website.


This method is the default implementation used by the SidebarCommands() menu item. This is found by adding the Toggle Sidebar menu item, then fetching it's selector like so:

let menu = NSApp.mainMenu!.items.first(where: { $0.title == "View" })!
let submenu = menu.submenu!.items.first(where: { $0.title == "Toggle Sidebar" })!
submenu.target // nil
submenu.action // #selector(toggleSidebar:)

This means that it will most likely be more consistent (and supported) than the other methods.

Related