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:
- If the sidebar is collapsed there is a list and detail view
- 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
) - 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 smallDivider()
-like line in the Toolbar.
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.
Code Example
I have distilled the problem down to a simple app with mostly toolbar content. In my original code I use two nested NavigationView
s, 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.
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.