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?

4 Answers 4

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())
}
Sign up to request clarification or add additional context in comments.

1 Comment

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).
8

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
}

1 Comment

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.
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.

1 Comment

Thanks for sharing @diogo, especially for showing what Apple is using under the hood!
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.