Markus Moltke
Markus Moltke

Reputation: 1151

SwiftUI - Navigation bar button not clickable after sheet has been presented

I have just started using SwiftUI a couple of weeks ago and i'm still learning. Today I ran into an issue.

When I present a sheet with a navigationBarItems-button and then dismiss the ModalView and return to the ContentView I find myself unable to click on the navigationBarItems-button again.

My code is as follows:

struct ContentView: View {
    
    @State var showSheet = false
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Test")
            }.sheet(isPresented: self.$showSheet) {
                ModalView()
            }.navigationBarItems(trailing:
                Button(action: {
                    self.showSheet = true
                }) {
                    Text("SecondView")
                }
            )
        }
    }
}

struct ModalView: View {
    
    @Environment(\.presentationMode) var presentation
    
    var body: some View {
        VStack {
            Button(action: {
                self.presentation.wrappedValue.dismiss()
            }) {
                Text("Dismiss")
            }
        }
    }
}

Upvotes: 54

Views: 11983

Answers (9)

Madiyar
Madiyar

Reputation: 1089

I added a small delay between two separate modal presentations (sheets, dialog, etc...) to allow the previous one to finish up (animations?). This solved my issue and could be an alternative solution to the other suggestions, but it's still not ideal:

struct ContentView: View {

@State var showSheet = false

var body: some View {
    NavigationStack {
        VStack {
            Text("Test")
        }.sheet(isPresented: self.$showSheet) {
            ModalView()
        }.toolbar {
            ToolbarItemGroup(placement: .navigationBarTrailing) {
                Button {
                    // Here
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                        self.showSheet = true
                    }
                } label: {
                    Text("SecondView")
                }
            }
        }
    }
}

Hopefully, this issue is already fixed in a recent version of iOS.

Upvotes: 0

Konstantin Cherkashin
Konstantin Cherkashin

Reputation: 146

The core issue is misaligned navigation bar item frame. The safe solution would be to force bar items to be recreated each time a modal closes. Yes, it's inefficient to recreate views, but since bar buttons are quite simple, such an overhead doesn't seem noticeable.

So, continuing a flash mob of finding their own workarounds, here's mine:

  1. In the view, place:
@Environment(\.presentationMode) var presentationMode

that would force the view to be updated each time a modal is presented or dismissed from it;

  1. Declare toolbar items with random IDs
ToolbarItem(id: UUID().uuidString, placement: .navigationBarLeading) { ... }

that way, they are recreated each time the view is updated (because of the new ID randomly generated each time)

Upvotes: 1

Nirvan Nagar
Nirvan Nagar

Reputation: 359

Only @adamwjohnson5's answer worked for me. I don't like doing it but it's the only solution that works as of Xcode 13.1 and iOS 15.0. Here is my code for anyone interested in seeing iOS 15.0 targeted code:

var body: some View {
    NavigationView {
        mainContentView
            .navigationTitle(viewModel.navigationTitle)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    PlusButton {
                        viewModel.showAddDialog.toggle()
                    }
                    .frame(height: 96, alignment: .trailing) // Workaroud, credit: https://stackoverflow.com/a/62209223/5421557
                    .confirmationDialog("CatalogView.Add.DialogTitle", isPresented: $viewModel.showAddDialog, titleVisibility: .visible) {
                        Button("Program") {
                            viewModel.navigateToAddProgramView.toggle()
                        }
                        
                        Button("Exercise") {
                            viewModel.navigateToAddExerciseView.toggle()
                        }
                    }
                }
            }
            .sheet(isPresented: $viewModel.navigateToAddProgramView, onDismiss: nil) {
                Text("Add Program View")
            }
            .sheet(isPresented: $viewModel.navigateToAddExerciseView, onDismiss: nil) {
                AddEditExerciseView(viewModel: AddEditExerciseViewModel())
            }
    }
    .navigationViewStyle(.stack)
}

Upvotes: 1

joltguy
joltguy

Reputation: 536

I'm still seeing this issue with Xcode 13 RC and iOS 15. Unfortunately the solutions above didn't work for me. What I ended up doing is adding a small Text view to the toolbar whose content changes depending on the value of the .showingSheet property.

struct ContentView: View {
    @State private var showingSheet = false

    var body: some View {
        NavigationView {
            VStack {
                Text("Content view")
                Text("Swift UI")
            }
            .sheet(isPresented: $showingSheet) {
                Text("This is a sheet")
            }
            .navigationTitle("Example")
            .toolbar {
                ToolbarItemGroup(placement: .navigationBarTrailing) {
                    // Text view workaround for SwiftUI bug
                    // Keep toolbar items tappable after dismissing sheet
                    Text(showingSheet ? " " : "").hidden()
                    Button(action: {
                        self.showingSheet = true
                    }) {
                        Label("Show Sheet", systemImage: "plus.square")
                    }
                }
            }
        }
    }
}

I realize it's not ideal but it's the first thing that worked for me. My guess is that having the Text view's content change depending on the .showingSheet property forces SwiftUI to fully refresh the toolbar group.

Upvotes: 5

Vahagn Gevorgyan
Vahagn Gevorgyan

Reputation: 2783

I think this happens because the presentationMode is not inherited from the presenter view, so the presenter didn't know that the modal is already closed. You can fix this by adding presentationMode to presenter, in this case to ContentView.

struct ContentView: View {

    @Environment(\.presentationMode) var presentation
    @State var showSheet = false

    var body: some View {
        NavigationView {
            VStack {
                Text("Test")
            }.sheet(isPresented: self.$showSheet) {
                ModalView()
            }.navigationBarItems(trailing:
                Button(action: {
                    self.showSheet = true
                }) {
                    Text("SecondView")
                }
            )
        }
    }
}

Tested on Xcode 12.5.

Here is the full working example.

Upvotes: 65

glassonion1
glassonion1

Reputation: 29

Very hacky but this worked for me:

I had the same problem. this solution worked for me.

struct ContentView: View {

    @State var showSheet = false

    var body: some View {
        NavigationView {
            VStack {
                Text("Test")
            }.sheet(isPresented: self.$showSheet) {
                ModalView()
            }.navigationBarItems(trailing:
                Button(action: {
                    self.showSheet = true
                }) {
                    Text("SecondView")
                        // this is a workaround
                        .frame(height: 96, alignment: .trailing)
                }
            )
        }
    }
}

Upvotes: 1

Ralf Ebert
Ralf Ebert

Reputation: 4082

This seems to be a bug in SwiftUI. I am also still seeing this issue with Xcode 11.5 / iOS 13.5.1. The navigationBarMode didn't make a difference.

I filed an issue with Apple:


FB7641003 - Taps on a navigationBarItem Button presenting a sheet sometimes not recognized

You can use the attached example project SwiftUISheet (also available via https://github.com/ralfebert/SwiftUISheet) to reproduce the issue. It just presents a sheet from a navigation bar button. Run the app and tap repeatedly on the 'plus' button in the nav bar. When the sheet pops up, dismiss it by sliding it down. Only some taps to the button will be handled, often a tap is ignored.

Tested on Xcode 11.4 (11E146) with iOS 13.4 (17E255).

Upvotes: 16

adamwjohnson5
adamwjohnson5

Reputation: 763

Very hacky but this worked for me:

Button(action: {
    self.showSheet = true
}) {
    Text("SecondView")
    .frame(height: 96, alignment: .trailing)
}

Upvotes: 7

Kyokook Hwang
Kyokook Hwang

Reputation: 2762

So far, I can still observe the disorder of navi buttons right after dismissing its presented sheet.

FYI, I am using a UINavigationController wrapper instead as workaround. It works well.

Unfortunately, I sure that the more that kind of bugs, the farther away the time of using SwiftUI widely by the ios dev guys. Because those are too basic to ignore.

Upvotes: 1

Related Questions