Why does SwiftUi view not update after value changed in popover called from swipeActions?

49 Views Asked by At

In the following code I would expect my ContentView to update after invoking the "Update" swipeAction which in turn enables updating the selected country population value. The correct value is returned but it does not trigger the ContentView to update the selected country's population value.

import SwiftUI

class Country: ObservableObject {
    var id = UUID()
    @Published var code: String
    @Published var population: Int
    
    init(code: String, population: Int) {
        self.code = code
        self.population = population
    }
}

typealias Countries = [Country]

struct ContentView: View {
    @State var showPopover = false
    @State var selectedCountry: Country = Country(code: "??", population: 0)
    @State var newValue: Int = 0
    @State var countries: Countries = Countries()

    var body: some View {
        List (countries, id: \.id) { country in
            HStack {Spacer(); Text(country.code); Spacer(); Text("\(country.population)"); Spacer()
            }.font(.title)
            .swipeActions(content: {
                Button { selectedCountry = country; showPopover = true } label: { Text("Update") }
            })
        }.popover(isPresented: $showPopover) {
            PopoverView(country: $selectedCountry)
        }.onAppear {
            if countries.isEmpty {
                countries.append(contentsOf:
                    [Country(code: "AU", population: 26000000),
                     Country(code: "US", population: 332000000),
                     Country(code: "UK", population: 67000000)
                    ])
            }
        }
    }
}

struct PopoverView: View {
    @Environment(\.presentationMode) var presentationMode

    @Binding var country: Country
    @State var value = "???"

    var body: some View {
        HStack {
            Spacer()
            Text(country.code)
            TextField(country.code, text: $value)
            Button {
                if let intValue = Int(value) { country.population = intValue }
                print("New population: \(country.population)")
               presentationMode.wrappedValue.dismiss()
            }
            label: { Text("DONE")}
            Spacer()
        }.onAppear {
            value = String(country.population)
        }
    }
}

I've tried everything suggested in a number of online articles but nothing seems to cause the ContentView to update the selected population

1

There are 1 best solutions below

0
MatBuompy On

I've digged into your issue and found a solution. Your solution doesn't work because you are editing the selectedCountry variable (and never really displaying it) and not the collection values themeselves. First, I needed to refactor your model like this:

class CountryContainer: ObservableObject {
    
    @Published var countries = [Country]()
    
    init(countries: [Country] = [Country]()) {
        self.countries = countries
    }
    
}

class Country: ObservableObject, Identifiable {
    
    var id = UUID()
    @Published var code: String
    @Published var population: Int
    
    init(code: String, population: Int) {
        self.code = code
        self.population = population
    }
    
}

Storing your countries into an ObservableObject class, I also made your Country class Identifiable since it already had an ID, so when looping through it we would not have any issues.

Then I've edited your Popover view to take in a function:

struct PopoverView: View {
    @Environment(\.presentationMode) var presentationMode
    
    @Binding var country: Country
    @State var value = "???"
    var onCountryChanged: () -> Void
    
    var body: some View {
        HStack {
            Spacer()
            Text(country.code)
            TextField(country.code, text: $value)
            Button {
                if let intValue = Int(value) { country.population = intValue }
                print("New population: \(country.population)")
                presentationMode.wrappedValue.dismiss()
                onCountryChanged()
            }
        label: { Text("DONE")}
            Spacer()
        }.onAppear {
            value = String(country.population)
        }
    }
}

I've called that function once the edit action was complete. I've then used your selectedCountry to edit the actual collection values stored in the countryContainer StateObject like so:

@State var showPopover = false
@State var selectedCountry: Country = Country(code: "??", population: 0)
@State var newValue: Int = 0
@StateObject var countryContainer = CountryContainer()

var body: some View {
    List (countryContainer.countries) { country in
        HStack {
            Spacer()
            Text(country.code)
            Spacer()
            Text("\(country.population)");
            Spacer()
        }
        .font(.title)
            .swipeActions(content: {
                Button {
                    selectedCountry = country
                    showPopover = true
                } label: { 
                    Text("Update")
                }
            })
    }.popover(isPresented: $showPopover) {
        PopoverView(country: $selectedCountry) {
            let countryToEditIndex = countryContainer.countries.firstIndex(where: { $0.id == selectedCountry.id })
            countryContainer.countries[countryToEditIndex!] = selectedCountry
        }
    }.onAppear {
        if countryContainer.countries.isEmpty {
            countryContainer.countries.append(contentsOf:
                                [Country(code: "AU", population: 26000000),
                                 Country(code: "US", population: 332000000),
                                 Country(code: "UK", population: 67000000)
                                ])
        }
    }
}

This solution is maybe not the best, and you can work out way to improve it, but I just wanted to point you in the right direction. I hope I could help you and my explaination was clear. Let me know!