avdyushin
avdyushin

Reputation: 1946

SwiftUI modal presentation works only once from navigationBarItems

Here is a bug in SwiftUI when you show modal from button inside navigation bar items. In code below Button 1 works as expected, but Button 2 works only once:

struct DetailView: View {

    @Binding var isPresented: Bool
    @Environment (\.presentationMode) var presentationMode

    var body: some View {
        NavigationView {
            Text("OK")
            .navigationBarTitle("Details")
            .navigationBarItems(trailing: Button(action: {
                self.isPresented = false
                // or:
                // self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Done").bold()
            })
        }
    }
}

struct ContentView: View {

    @State var showSheetView = false

    var body: some View {
        NavigationView {
            Group {
                Text("Master")
                Button(action: { self.showSheetView.toggle() }) {
                    Text("Button 1")
                }
            }
            .navigationBarTitle("Main")
            .navigationBarItems(trailing: Button(action: {
                self.showSheetView.toggle()
            }) {
                Text("Button 2").bold()
            })
        }.sheet(isPresented: $showSheetView) {
            DetailView(isPresented: self.$showSheetView)
        }
    }
}

This bug is from the middle of the last year, and it still in Xcode 11.3.1 + iOS 13.3 Simulator and iOS 13.3.1 iPhone XS.

Is here any workaround to make button work?

EDIT:

  1. Seems to be tap area goes somewhere down and it's possible to tap below button to show modal.

Temporary solution to this is to use inline navigation bar mode: .navigationBarTitle("Main", displayMode: .inline)

Upvotes: 9

Views: 3180

Answers (2)

Metin Atalay
Metin Atalay

Reputation: 1517

Thanks for the Asperi answer but there is still an error.

He said to use id, but when there is a scroll in the NavigationView and we refresh this id, the scroll comes to the top.

You can use the below solution.

content
        .onDisappear {
            let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
            if let viewFrame = scene?.windows.first?.rootViewController?.view.frame {
                scene?.windows.first?.rootViewController?.view.frame = .zero
                scene?.windows.first?.rootViewController?.view.frame = viewFrame
            }
        }

Upvotes: 0

Asperi
Asperi

Reputation: 257711

Well, the issue is in bad layout (seems broken constrains) of navigation bar button after sheet has closed

It is clearly visible in view hierarchy debug:

demo

Here is a fix (workaround of course, but safe, because even after issue be fixed it will continue working). The idea is not to fight with broken layout but just create another button, so layout engine itself remove old-bad button and add new one refreshing layout. The instrument for this is pretty known - use .id()

demo2

So modified code:

struct ContentView: View {

    @State var showSheetView = false
    @State private var navigationButtonID = UUID()
    
    var body: some View {
        NavigationView {
            Group {
                Text("Master")
                Button(action: { self.showSheetView.toggle() }) {
                    Text("Button 1")
                }
            }
            .navigationBarTitle("Main")
            .navigationBarItems(trailing: Button(action: {
                self.showSheetView.toggle()
            }) {
                Text("Button 2").bold() // recommend .padding(.vertical) here
            }
            .id(self.navigationButtonID)) // force new instance creation
        }
        .sheet(isPresented: $showSheetView) {
            DetailView(isPresented: self.$showSheetView)
                .onDisappear {
                    // update button id after sheet got closed
                    self.navigationButtonID = UUID()
                }
        }
    }
}

Upvotes: 23

Related Questions