SwiftUI macOS Xcode Style Toolbar
Asked Answered
L

3

9

I like to recreate a toolbar similar to Apples Notes App using SwiftUI in a macOS app (I am using Xcode 12.3 and macOS 11.1): enter image description here

My attempt was to use a Navigation View to get the Master/Detail setup (for now I do not need a third panel like the original Notes App has). I am interested in how to get the appearance right, e.g. background color and behavior of the buttons in the toolbar. I tried out some approaches, the best I came up with for the moment is this for the main file:

import SwiftUI

@main
struct App_Without_Name_in_Window_Top_AreaApp: App {
    var body: some Scene {
        WindowGroup("") { // <-- The ("") will remove the app name in the toolbar
            ContentView()
        }
        .windowToolbarStyle(UnifiedCompactWindowToolbarStyle())
    }
}

And for the content view:

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            Text("Master")
                .frame(minWidth: 200, maxWidth: 300, minHeight: 300, alignment: .leading)
                .padding()
                .toolbar {
                    ToolbarItem(placement: .status) {
                        Button(action: {
                            myToggleSidebar()
                        }) {
                            Image(systemName: "sidebar.left")
                        }
                    }
                }
                .presentedWindowToolbarStyle(ExpandedWindowToolbarStyle())
            
            
            Text("Detail")
                .frame(minWidth: 200, alignment: .center)
                .toolbar {
                    ToolbarItem(placement: .navigation) {
                        Button(action: {
                            print("Button pressed")
                        }) {
                            Image(systemName: "bold.italic.underline")
                        }
                    }
                    
                    ToolbarItem(placement: .navigation) {
                        Button(action: {
                            print("Button pressed")
                        }) {
                            Image(systemName: "lock")
                        }
                    }
                }
        }
        .frame(minWidth: 500, minHeight: 300)
    }
}

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

which yields a result like this: enter image description here

Now my question is: How can I alter the color of the left and right parts of the toolbar? I also have problems with the behavior of the toolbar. When the master panel's size is increased, the buttons of the right part of the toolbar are disappearing very early although there is a lot of space left: enter image description here

What do I have to do to prevent it?

Litalitany answered 26/12, 2020 at 9:4 Comment(0)
G
11

Okay, I found a trick that works:

  1. Set the scene's windowStyle to HiddenTitleBarWindowStyle, which both hides the title and removes the white background:
WindowGroup {
    ContentView()
}
.windowToolbarStyle(UnifiedCompactWindowToolbarStyle())
.windowStyle(HiddenTitleBarWindowStyle())

(Note that I don't set the scene name to an empty string, as that's no longer needed and it messed up the window name in the "Window" menu too)

  1. To force a divider between the toolbar and the detail view content, stretch the detail content to fill the whole space and put a Divider behind it:
Text("Detail")
    .frame(minWidth: 200, maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
    .background(VStack {
        Divider()
        Spacer()
    })
    .toolbar { ...

That seems to do it!

enter image description here

Godhood answered 1/1, 2021 at 0:52 Comment(2)
I should mention: if you change your detail view to a ScrollView, the content will scroll over top of the toolbar content because we've set the window to have a hidden titlebar. Setting .clipped() on the ScrollView seems to help, but you may run into other issues so watch out. :)Godhood
Be careful of what it will look like if you open a new tabCurlicue
C
8

What you want is to use

.windowToolbarStyle(UnifiedWindowToolbarStyle(showsTitle: false))

because it preserves the correct behavior when the user tabs the application

Good

Using

.windowToolbarStyle(UnifiedCompactWindowToolbarStyle())
.windowStyle(HiddenTitleBarWindowStyle())

Causes funky behavior when the user opens a new tab due to the coloring of the toolbar.

funky

Curlicue answered 7/1, 2021 at 21:11 Comment(2)
Have you tested with a sidebar? Setting the toolbar style to UnifiedWindowToolbarStyle(showsTitle: false) still shows a white background in the toolbar on the detail view side, which is what @Litalitany was trying to avoid. If you put a divider just below the toolbar, as I suggest in my answer, then the tabs look okay to me.Godhood
Yes I've tested it, it's as you described. From what I read they asked for background color and toolbar similar to Apple notes app, which is "white" on the right side and an NSMaterialEffects.Material.sidebar on the leftCurlicue
C
0

Try this:

    internal func setupToolbar() {
    let toolbar = NSToolbar(identifier: UUID().uuidString)
    toolbar.delegate = self
    toolbar.displayMode = .labelOnly
    toolbar.showsBaselineSeparator = false
    toolbar.allowsUserCustomization = true
    toolbar.autosavesConfiguration = true
    toolbar.sizeMode = .small
    toolbar.allowsExtensionItems = true
    if #available(macOS 15.0, *) {
        toolbar.allowsDisplayModeCustomization = true
    } else {
            // Fallback on earlier versions
    }
    window?.titleVisibility = toolbarCollapsed ? .visible : .hidden
    window?.toolbarStyle = .unifiedCompact
    window?.titlebarSeparatorStyle = .automatic
    window?.toolbar = toolbar
}

and in a controller file in a init method call setupToolbar like this:

 final class yourcontroller: NSWindowController, NSToolbarDelegate {
    init(...){
      .......
      setupToolbar()
    }
}

for a toolbar icon generic you can you something like this:

    import SwiftUI

    struct ToolBarIconView: View {

    private var iconName: String
    private var accessibilityDescription: String
    private var withPadding: Bool?
    private var action: () -> Void

    init(
        iconName: String, accessibilityDescription: String? = String.Empty, withPadding: Bool? = false,
        action: @escaping () -> Void = {}
    ) {
        self.iconName = iconName
        self.accessibilityDescription = accessibilityDescription!
        self.withPadding = withPadding
        self.action = action
    }

    var body: some View {
        if #available(macOS 14.0, *) {
            Button(
                action: action,
                label: {
                    Image(
                        nsImage: (NSImage(
                            systemSymbolName: self.iconName,
                            accessibilityDescription: self.accessibilityDescription)?
                            .withSymbolConfiguration(.init(scale: .large))!)!
                    )
                }
            )

            .background(
                Color(NSColor.systemFill),
                in: RoundedRectangle(cornerRadius: 4.5, style: .continuous)
            )
            .buttonStyle(.bordered)
        } else {
                // Fallback on earlier versions
        }
    }
}

result:

toolbar icon

Central answered 31/8 at 17:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.