Search code examples
swiftuiswiftui-listswiftui-navigationlinkswiftui-navigationview

Popover displaying inaccurate information inside ForEach


I'm having a problem where I have a ForEach loop inside a NavigationView. When I click the Edit button, and then click the pencil image at the right hand side on each row, I want it to display the text variable we are using from the ForEach loop. But when I click the pencil image for the text other than test123, it still displays the text test123 and I have absolutely no idea why.

Here's a video. Why is this happening?

import SwiftUI

struct TestPopOver: View {
    
    private var stringObjects = ["test123", "helloworld", "reddit"]
    
    @State private var editMode: EditMode = .inactive
    @State private var showThemeEditor = false
        
    @ViewBuilder
    var body: some View {
        NavigationView {
            List {
                ForEach(self.stringObjects, id: \.self) { text in
                    NavigationLink( destination: HStack{Text("Test!")}) {
                        HStack {
                            Text(text)
                            Spacer()
                            if self.editMode.isEditing {
                                Image(systemName: "pencil.circle").imageScale(.large)
                                    .onTapGesture {
                                        if self.editMode.isEditing {
                                            self.showThemeEditor = true
                                        }
                                    }
                            }
                            
                        }
                    }
                    .popover(isPresented: $showThemeEditor) {
                        CustomPopOver(isShowing: $showThemeEditor, text: text)
                    }
                    
                }
            }
            .navigationBarTitle("Reproduce Editing Bug!")
            .navigationBarItems(leading: EditButton())
            .environment(\.editMode, $editMode)

        }
    }
}

struct CustomPopOver: View {
    @Binding var isShowing: Bool
    var text: String
    var body: some View {
        VStack(spacing: 0) {
            HStack() {
                Spacer()
                Button("Cancel") {
                    self.isShowing = false
                }.padding()
            }
            Divider()
            List {
                Section {
                    Text(text)
                }
            }.listStyle(GroupedListStyle())
        }
        
    }
}

Solution

  • This is a very common issue (especially since iOS 14) that gets run into a lot with sheet but affects popover as well.

    You can avoid it by using popover(item:) rather than isPresented. In this scenario, it'll actually use the latest values, not just the one that was present when then view first renders or when it is first set.

    struct EditItem : Identifiable { //this will tell it what sheet to present
        var id = UUID()
        var str : String
    }
    
    struct ContentView: View {
        
        private var stringObjects = ["test123", "helloworld", "reddit"]
        
        @State private var editMode: EditMode = .inactive
        @State private var editItem : EditItem? //the currently presented sheet -- nil if no sheet is presented
        
        @ViewBuilder
        var body: some View {
            NavigationView {
                List {
                    ForEach(self.stringObjects, id: \.self) { text in
                        NavigationLink( destination: HStack{Text("Test!")}) {
                            HStack {
                                Text(text)
                                Spacer()
                                if self.editMode.isEditing {
                                    Image(systemName: "pencil.circle").imageScale(.large)
                                        .onTapGesture {
                                            if self.editMode.isEditing {
                                                self.editItem = EditItem(str: text) //set the current item
                                            }
                                        }
                                }
                                
                            }
                        }
                        .popover(item: $editItem) { item in //item is now a reference to the current item being presented
                            CustomPopOver(text: item.str)
                        }
                        
                    }
                }
                .navigationBarTitle("Reproduce Editing Bug!")
                .navigationBarItems(leading: EditButton())
                .environment(\.editMode, $editMode)
    
            }.navigationViewStyle(StackNavigationViewStyle())
        }
    }
    
    struct CustomPopOver: View {
        @Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
    
        var text: String
        var body: some View {
            VStack(spacing: 0) {
                HStack() {
                    Spacer()
                    Button("Cancel") {
                        self.presentationMode.wrappedValue.dismiss()
                    }.padding()
                }
                Divider()
                List {
                    Section {
                        Text(text)
                    }
                }.listStyle(GroupedListStyle())
            }
            
        }
    }
    

    I also opted to use the presentationMode environment property to dismiss the popover, but you could pass the editItem binding and set it to nil as well (@Binding var editItem : EditItem? and editItem = nil). The former is just a little more idiomatic.