12

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?

ricobeck
  • 172
  • 1
  • 11

4 Answers4

15

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())
}
IrelDev
  • 317
  • 5
  • 12
  • I am fairly new to SwiftUI (not a newbie programmer though); it seems, that just attaching `.commands { SidebarCommands() }` to the `WindowGroup` did the trick (I already had a `NavigationView`, but it vanished). Thanks for your suggestion and help (tried on XCode 14.3, MacOS 13.x). – Igor Apr 24 '23 at 00:00
8

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.

diogo
  • 511
  • 4
  • 5
6

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
}
  • 2
    It doesn't work properly if you use the three panels layout (sidebar, primary, detail). `NSApp.keyWindow?.contentViewController?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil)` is better at this and will work also if the primary or detail panel or one of their subviews is the first responder. – Pyroh Nov 06 '20 at 21:16
1

I don't use NavigationView for this. Use a HSplitView instead and use a hidden flag like in https://stackoverflow.com/a/59228385/811010

Stephan Michels
  • 952
  • 4
  • 18