Reputation: 251
I've added a .toolbar to the top level of a NavigationView that will eventually be used to select items in a list without using swipe gestures (up button, down button, etc.). I also have a .navigationBar going on, to access other views for Account and Settings.
For the most part it's looking really good, but when I follow a NavigationLink (in .navigationBarItems) within NavigationView, and then use the built-in back navigation, my .toolbar disappears from the top level.
Am I putting the .toolbar in the wrong place? It feels like a problem with .navigationViewStyle(StackNavigationViewStyle()) because when I comment that out, the toolbar will not disappear upon navigation... but I don't like how the default behavior works in landscape so I'm relying on it.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List {
Group {
Section(header: Text("List Items").foregroundColor(.gray).font(.footnote)) {
Text("List Item One")
Text("List Item Two")
Text("List Item Three")
}
}
}.navigationTitle("Top Level List").navigationBarTitleDisplayMode(.inline)
.ignoresSafeArea(.all)
// MARK: NAVBAR
.navigationBarItems(
leading:
NavigationLink(destination: UserView()) {
Image(systemName: "person.crop.circle").font(.title2)
},
trailing:
NavigationLink(destination: SettingsView()) {
Image(systemName: "gear").font(.title2)
})
//MARK: - CONTENT NAV
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {}, label: {Label("Mute", systemImage: "speaker.slash.fill")})
Spacer()
Button(action: {}, label: {Label("Repeat", systemImage: "arrow.clockwise")})
Spacer()
Button(action: {}, label: {Label("Previous", systemImage: "arrow.up")})
Spacer()
Button(action: {}, label: {Label("Next", systemImage: "arrow.down")})
Spacer()
Button(action: {}, label: {Label("Select", systemImage: "arrow.right")})
}
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct UserView: View {
@State private var username: String = ""
@State private var password: String = ""
var body: some View {
Form {
TextField("Username", text: $username)
SecureField("Password", text: $password)
}
.navigationBarTitle("Account").font(.subheadline)
}
}
struct SettingsView: View {
@State private var setting1: String = ""
@State private var setting2: String = ""
var body: some View {
Form {
TextField("Setting One", text: $setting1)
SecureField("Setting Two", text: $setting2)
}
.navigationBarTitle("Settings").font(.subheadline)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Upvotes: 18
Views: 4222
Reputation: 304
I found a more SwiftUI adapted approach to solve the problem of the disappearing toolbar (navigation bar) in SwiftUI and iOS. I use the toolbar for very essential commands - a replacement of the application menu in macOS. So, an absent toolbar cripples my iOS application. It should never(!) happen. However a variety of situations made it disappearing (like when I dismiss a pop-up view or scrolling in the main view). For this, I seemed to have found a solid solution. First the template code to generate the toolbar - this is symbolic code for generating the basic content view in my application.
struct ContentView: View {
var body: some View {
// The NavigationStack ensures the presence of the navigationBar (toolBar)
let theView =
NavigationStack(root: {self.generateContentView()})
return theView
}
func generateContentView() -> some View {
let theView =
Text("This is my content view")
.toolbar {self.generateToolBar()}
}
@ToolbarContentBuilder
func generateToolBar() -> some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) {
Text("This is a toolbar item")
}
}
}
My solution was that if I can not prevent the system from making these decisions not to show the toolbar I can try to undo the hiding by setting a State variable navigationBarIsHidden to false.
Here the updated code setting the State variable naviationBarIsHidden to false upon receiving a notification "renewToolbar". Note the modifier .navigationBarHidden(self.navigationBarIsHidden)
for the ContentView and the delayed change of navigationBarIsHidden to false via a Task()
.
struct ContentView: View {
let document:ApplicationDocument
@State var navigationBarIsHidden : Bool = false
var body: some View {
// The NavigationStack ensures the presence of the navigationBar (toolBar)
let theView =
NavigationStack(root: {self.generateContentView()})
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: "renewToolbar"), object: self.document), perform: { notification in
self.navigationBarIsHidden = true
Task() {@MainActor () -> Void in
try? await Task.sleep(for:.milliseconds(1000))
self.navigationBarIsHidden = false
}})
return theView
}
func generateContentView() -> some View {
let theView =
Text("This is my content view")
.toolbar {self.generateToolBar()}
.navigationBarHidden(self.navigationBarIsHidden)
}
@ToolbarContentBuilder
func generateToolBar() -> some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) {
Text("This is a toolbar item")
}
}
}
The question remained: when should I send this notification if such a variety of events can cause the navigation bar to disappear ? The solution was to bind this to a .onDisappear modifier of one of the Toolbar items themself.
So here is the complete code:
struct ContentView: View {
let document:ApplicationDocument
@State var navigationBarIsHidden : Bool = false
var body: some View {
// The NavigationStack ensures the presence of the navigationBar (toolBar)
let theView =
NavigationStack(root: {self.generateContentView()})
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: "renewToolbar"), object: self.document), perform: { notification in
self.navigationBarIsHidden.toggle()
Task() {@MainActor () -> Void in
try? await Task.sleep(for:.milliseconds(1000))
self.navigationBarIsHidden.toggle()
}})
return theView
}
func generateContentView() -> some View {
let theView =
Text("This is my content view")
.toolbar {self.generateToolBar()}
.navigationBarHidden(self.navigationBarIsHidden)
}
@ToolbarContentBuilder
func generateToolBar() -> some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) {
Text("This is a toolbar item")
.onDisappear(perform: {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "renewToolbar"), object: self.document)
})
}
}
}
In my real application I have the notification actually bound to a TextField (a search field) not to a Text, just in case this is not called for a Text.
The result is that the toolbar still disappears when I dismiss a pop-up view but it reappears immediately thereafter.
I tried the solution with renewing the id of a view as proposed above. It did not work for me. Replacing an id is a heavy action in SwiftUI because if force the re-rendering of the entire view hierarchy. The entire view (in my case: the entire screen) rebuilds.
Upvotes: 1
Reputation: 8387
You right, it's in a wrong place. Here is how it should be if you need a toolbar always shown:
struct ContentView: View {
var body: some View {
NavigationView {
List {
Group {
Section(header: Text("List Items").foregroundColor(.gray).font(.footnote)) {
Text("List Item One")
Text("List Item Two")
Text("List Item Three")
}
}
}.navigationTitle("Top Level List").navigationBarTitleDisplayMode(.inline)
.ignoresSafeArea(.all)
// MARK: NAVBAR
.navigationBarItems(
leading:
NavigationLink(destination: UserView()) {
Image(systemName: "person.crop.circle").font(.title2)
},
trailing:
NavigationLink(destination: SettingsView()) {
Image(systemName: "gear").font(.title2)
})
//MARK: - CONTENT NAV
}.navigationViewStyle(StackNavigationViewStyle())
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {}, label: {Label("Mute", systemImage: "speaker.slash.fill")})
Spacer()
Button(action: {}, label: {Label("Repeat", systemImage: "arrow.clockwise")})
Spacer()
Button(action: {}, label: {Label("Previous", systemImage: "arrow.up")})
Spacer()
Button(action: {}, label: {Label("Next", systemImage: "arrow.down")})
Spacer()
Button(action: {}, label: {Label("Select", systemImage: "arrow.right")})
}
}
}
}
Upvotes: 1