georgeok
georgeok

Reputation: 5736

swiftUI bottomBar toolbar disappears when going back

I have these swiftUI views and trying to use the toolbar (bottomBar). When you launch the app it appears fine, but after going to View2 using he navigationLink and then go back to the main view the toolbar disappears. It happens when the NavigationLink being inside the list. If you don't use a list (put the navigation link inside a VStack or similar) it works as expected and the toolbar reappears when you go back to the initial view. Is there a way to fix this?enter image description here

import SwiftUI

struct View2: View {
    var body: some View {
        VStack{
            Text("View2")
        }
        
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView{
            List{
                NavigationLink(destination: View2()) {
                    Text("go to View2")
                }
                
            }
            .toolbar(content: {
                ToolbarItem(placement: .bottomBar, content: {
                    Text("toolbar item 1")
                })
            })
        }
        .navigationViewStyle(StackNavigationViewStyle())
            
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Upvotes: 7

Views: 3889

Answers (4)

M Wilm
M Wilm

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() {
               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() {
               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: 0

Asperi
Asperi

Reputation: 258621

Update: Fixed in Xcode 13.3 / iOS 15.4

This is known bug. Here is possible workaround - force refresh on View2 disappeared (tested with Xcode 12.1 / iOS 14.1)

struct ContentView: View {
    @State private var refresh = UUID()

    var body: some View {
        NavigationView{
            List{
                NavigationLink(destination:
                        View2().onDisappear { refresh = UUID() }) { // << here !!
                    Text("go to View2")
                }
            }
            .toolbar(content: {
                 ToolbarItem(placement: .bottomBar, content: {
                      Text("toolbar item 1")
                 })
            }).id(refresh)                     // << here !!

        }
        .navigationViewStyle(StackNavigationViewStyle())
    }
}

Upvotes: 16

gaohomway
gaohomway

Reputation: 4100

This way is simpler and will help some people.

.toolbar {
      ToolbarItem(id: UUID().uuidString, placement: .bottomBar, showsByDefault: true) {
            AssetToolbarView(selectedCount: 0)
      }        
}

Upvotes: 2

niklas30
niklas30

Reputation: 59

In my case I had to make sure that the toolbar is only visible in View1. To get this working I used a state variable to detect when View1 is viewed to the user.

 struct View2: View {
    var body: some View {
        Text("View2")
    }
 }

struct View1: View {
    @State private var refresh = UUID()
    @State var isShown = true
    
    var body: some View {
            NavigationView {
                List {
                    NavigationLink(destination:
                        View2()
                            .onDisappear(perform: {
                                    if (isShown) {
                                        refresh = UUID()
                                    }
                            })
                            .onAppear(perform: { isShown = false })
                    ){
                       Text("Row1")
                    }
                }
                .onAppear(perform: {
                    isShown = true
                })
                .toolbar(content: {
                    ToolbarItem(placement: .bottomBar) {
                    Button(action: {
                        // Action
                    }) {
                        HStack(spacing: 10) {
                        Image(systemName: "plus.circle")
                        Text("Add")
                    }
                    }}
                })
                .id(refresh)
            }
    }
}

Upvotes: 0

Related Questions