swiftswiftuitaskswiftui-navigationstackswiftui-ontapgesture

Why does swift navigation list with button open a sheet multiple times instead of just once?


TLDR: In the following code, clicking on the button from the top level ContentView opens multiple sheets, and then when closing the sheet it gets triggered again; yet clicking it from the RowView only opens one sheet. How can I only open one sheet from ContentView?

Here's a simple version of what I have: A navigation stack with a list of items, each of which has a button that will open a sheet. When the sheet is pulled up, it runs a task to call a web service and retrieve some data to display.

The issue is, according to my debugging, multiple sheets are being opened, each of which runs the task, and some fail due to the duplicate call. The way I'd like to fix this issue is to have the sheet only appear once. How can I get this to work?


import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink {
                    Text("Hello")
                } label: {
                    RowView(textToShow: "Text 1")
                }
                NavigationLink {
                    Text("Hello")
                } label: {
                    RowView(textToShow: "Text 2")
                }
            }
        }
    }
}

struct RowView: View {
    var textToShow: String
    @State private var showingPopover = false
    
    var body: some View {
        Text("Hello")
        
        Button(action: {
            //showingPopover = true
            print("Button pressed")
        }, label: {
            Image(systemName:"doc.text")
                .imageScale(.large)
                
        })
        .onTapGesture {
            print("Button tapped")
            showingPopover = true
        }
        .sheet(isPresented: $showingPopover, content: {
            PopupView(textToShow: textToShow)
                .onAppear {
                    print("Sheet appeared")
                }
        })
    }
}

struct PopupView: View {
    var textToShow: String
    @State private var loading = true
    var body: some View {
        VStack {
            if loading {
                HStack{
                    ProgressView()
                    Text("Loading...")
                }
            } else {
                Text(textToShow)
            }
        }
        .onAppear() {
            print("Loading")
            loading = false
        }
    }
}

#Preview {
    ContentView()
}

#Preview {
    RowView(textToShow: "Text 1")
}

Clicking the button, and then closing the sheet leads to the following in the console:

Button tapped
Loading
Sheet appeared
Loading
Sheet appeared
Loading
Sheet appeared

I've tried using a state variable to capture when the popover has already been opened, and then for the sheet content to only display if the popover hasn't been opened; I tried setting the binding variable to true only on the first tap gesture, but that's not the fix either.

Based on another similar question, I think I need to move the sheet up, which is a little tricky then to capture the row-specific text (I've been using swift for like 2 weeks now? so not completely used to it) but I think that could work

I'm expecting only a single sheet to load and run its task.


Solution

  • Ok I think I found a solution: by moving the sheet up to the NavigationStack, it only appears once. To make the comment come from a variable that is Row-dependent, I just use a binding variable. In this case I used isPresented and a separate variable, but in my own case I think I will use the item input for a sheet.

    import SwiftUI
    
    struct ContentView: View {
        @State var showingPopover: Bool = false
        @State var textToShow: String = ""
        var body: some View {
            NavigationStack {
                List {
                    NavigationLink {
                        Text("Hello")
                    } label: {
                        RowView(showingPopover: $showingPopover, textToShow: $textToShow, textValue: "Text 1")
                    }
                    
                    NavigationLink {
                        Text("Hello")
                    } label: {
                        RowView(showingPopover: $showingPopover, textToShow: $textToShow, textValue: "Text 2")
                    }
                }
            }
            .sheet(isPresented: $showingPopover, onDismiss: {
                print("Sheet dismissed")
                showingPopover = false
                textToShow = ""
            }, content: {
                    PopupView(textToShow: textToShow)
                        .onAppear {
                            print("Popup appeared")
                        }
            })
        }
        
    }
    
    struct RowView: View {
        @Binding var showingPopover: Bool
        @Binding var textToShow: String
        var textValue: String
        
        var body: some View {
            Text("Hello")
            
            Button(action: {
                //showingPopover = true
                print("Button pressed")
            }, label: {
                Image(systemName:"doc.text")
                    .imageScale(.large)
                    
            })
            .onTapGesture {
                print("Button tapped")
                textToShow = textValue
                showingPopover = true
            }
            
        }
    }
    
    struct PopupView: View {
        var textToShow: String
        @State private var loading = true
        
        var body: some View {
            VStack {
                if loading {
                    HStack{
                        ProgressView()
                        Text("Loading...")
                    }
                } else {
                    Text(textToShow)
                }
            }
            .onAppear() {
                print("Loading")
                loading = false
            }
        }
    }
    
    #Preview {
        ContentView()
    }