Enie
Enie

Reputation: 699

how to add a toolbar divider for a three column view in swiftUI life cycle

I am looking for a way to achieve a Toolbar for a three column layout like Mail.app. Also the Notes.app uses almost the same Toolbar with the only important difference between the two apps being that Notes.app looks like it's WindowStyle is a HiddenTitleBarWindowStyle and Mail.app looks like a Default|TitleBarWindowStyle.

Following should be true:

  1. If the sidebar is collapsed there is a list and detail view
  2. The dividing line that separates the list from the detail view goes all the way up through the toolbar. (This could be achieved with a HiddenTitleBarWindowStyle) sidebar collapsed - fully divided view
  3. If the Title is too long to fit into the navigation list, the vertical dividing line will be broken: The list is still divided from the Detail View as before, but now the Toolbar looks like a DefaultWindowStyle with only a small Divider()-like line in the Toolbar. sidebar collapsed - divider line

Which combination of WindowStyle, WindowToolbarStyle and .toolbar configuration do I need to achieve this setup?

EDIT

I noticed that it is not possible to remove the divider line that Notes.app shows. I have not found a references to any such element in the docs though.

Notes.app Customizable Toolbar

Code Example

I have distilled the problem down to a simple app with mostly toolbar content. In my original code I use two nested NavigationViews, while for the example I used only one NavigationView with two Lists. However the Toolbar results are the same.

Toolbar with DefaultWindowStyle

import SwiftUI

@main
struct ToolbarTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView(titleBarIsHidden: true)
        }
        .windowToolbarStyle(UnifiedWindowToolbarStyle())
        .commands {
            SidebarCommands()
        }
    }
}

struct ContentView: View {
    @State var destination: String = "Toolbar Test"
    @State var detail: String = ""
    
    var body: some View {
        NavigationView {
            List {
                Button(action: {self.destination = "Item with the identifier:  1"}, label: {
                    Text("Item 1")
                })
                .buttonStyle(DefaultButtonStyle())
                Button(action: {self.destination = "Item 2"}, label: {
                    Text("Item 2")
                })
                .buttonStyle(DefaultButtonStyle())
            }
            .listStyle(SidebarListStyle())
            List {
                NavigationLink(
                    destination: DetailView(text: "\(destination) – \(detail)").onAppear{self.detail = "Detail 1"},
                    label: {
                        Text("\(destination) – Detail 1")
                    })
                NavigationLink(
                    destination: DetailView(text: "\(destination) – \(detail)").onAppear{self.detail = "Detail 2"},
                    label: {
                        Text("\(destination) – Detail 2")
                    })
            }
            .listStyle(InsetListStyle())
            Text("\(destination) – \(detail)")
        }
        .navigationTitle(destination)
        .navigationSubtitle(detail)
        .toolbar(id: "nav") {
            ToolbarItem(id: "plus", placement: ToolbarItemPlacement.principal, showsByDefault: true) {
                HStack {
                    Button(action: {print("pressed")}, label: {
                        Image(systemName: "plus.circle")
                    })
                }
            }
            ToolbarItem(id: "spacer", placement: ToolbarItemPlacement.confirmationAction, showsByDefault: true) {
                HStack {
                    Spacer()
                }
            }
            ToolbarItem(id: "sidebar.end", placement: ToolbarItemPlacement.confirmationAction, showsByDefault: true) {
                Button(action: {print("pressed")}, label: {
                    Image(systemName: "sidebar.right")
                })
            }
        }
    }
}

This example will result in a Toolbar that will never show the divider line that splits the whole Toolbar in two parts. Also the first ToolbarItem is positioned in the center of the Toolbar. I tried all ToolbarItemPlacement but none caused the item to move to the far left adjacent to the title.

Default Window Style

Toolbar with HiddenTitleBarWindowStyle

@main
struct ToolbarTestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentViewForHiddenTitleBar()
        }
        .windowStyle(HiddenTitleBarWindowStyle()) // added hidden title style
        .windowToolbarStyle(UnifiedWindowToolbarStyle())
        .commands {
            SidebarCommands()
        }
    }
}

struct ContentViewForHiddenTitleBar: View {
    @State var destination: String = "Toolbar Test"
    @State var detail: String = ""
    
    var body: some View {
        NavigationView {
            List {
                Button(action: {self.destination = "Item with the identifier:  1"}, label: {
                    Text("Item 1")
                })
                .buttonStyle(DefaultButtonStyle())
                Button(action: {self.destination = "Item 2"}, label: {
                    Text("Item 2")
                })
                .buttonStyle(DefaultButtonStyle())
            }
            .listStyle(SidebarListStyle())
            // add geometry reader to trim title width in toolbar
            GeometryReader { geometry in
                List {
                    NavigationLink(
                        destination: DetailView(text: "\(destination) – \(detail)").onAppear{self.detail = "Detail 1"},
                        label: {
                            Text("\(destination) – Detail 1")
                        })
                    NavigationLink(
                        destination: DetailView(text: "\(destination) – \(detail)").onAppear{self.detail = "Detail 2"},
                        label: {
                            Text("\(destination) – Detail 2")
                        })
                }
                // there is no title anymore so let's fake it.
                .toolbar(id: "list navigation") {
                    ToolbarItem(id: "title", placement: ToolbarItemPlacement.navigation, showsByDefault: true) {
                        VStack(alignment: .leading) {
                            Text(destination)
                                .font(.headline)
                                .frame(maxWidth: .infinity, alignment: .leading)
                            Text(detail)
                                .font(.subheadline)
                                .opacity(0.6)
                                .frame(maxWidth: .infinity, alignment: .leading)
                        }
                        .frame(width: geometry.size.width)
                    }
                }
            }
            .listStyle(InsetListStyle())
            Text("\(destination) – \(detail)")
        }
        .navigationTitle(destination)
        .navigationSubtitle(detail)
        .toolbar(id: "nav") {
            // primary action will place the item next to the divider line.
            ToolbarItem(id: "plus", placement: ToolbarItemPlacement.primaryAction, showsByDefault: true) {
                HStack {
                    Button(action: {print("pressed")}, label: {
                        Image(systemName: "plus.circle")
                    })
                }
            }
            ToolbarItem(id: "spacer", placement: ToolbarItemPlacement.confirmationAction, showsByDefault: true) {
                HStack {
                    Spacer()
                }
            }
            ToolbarItem(id: "sidebar.end", placement: ToolbarItemPlacement.confirmationAction, showsByDefault: true) {
                Button(action: {print("pressed")}, label: {
                    Image(systemName: "sidebar.right")
                })
            }
        }
    }
}

This example will result in a Toolbar that will always show a full height divider. Even if the title is too long. Therefore a GeometryReader was added. This is fine until the sidebar will collapse. The placement of ToolbarItems will not be correct. Also when customising the Toolbar there would be the possibility to remove the title, which should not be possible.

Hidden Title Bar Window Style Hidden Title Bar Window Style with Collapsed Sidebar

Upvotes: 5

Views: 2812

Answers (1)

Adam
Adam

Reputation: 5105

The default title bar style is fine, you just need to attach your toolbar items to the subviews of your top NavigationView, e.g.:

var body: some View {
    NavigationView {
        
        List {
            ...
        }
        .listStyle(SidebarListStyle())
        .toolbar {
            ToolbarItem {
                Button(action: { }, label: {
                    Image(systemName: "sidebar.right")
                })
            }
        }
        
        List {
            ...
        }
        .listStyle(InsetListStyle())
        .toolbar {
            ToolbarItem {
                Button(action: { }, label: {
                    Image(systemName: "plus.circle")
                })
            }
        }
        
        Text("\(destination) – \(detail)")
    }
    .navigationTitle(destination)
    .navigationSubtitle(detail)
}

I didn’t attach any toolbar items to the third column (the Text) but you can — just be sure to attach the same toolbar items to your DetailViews, as its toolbar will replace the Text’s toolbar when the user navigates. (if you’re not sure what I mean, just try it and you’ll quickly see what I’m talking about :)

Upvotes: 3

Related Questions