Home > Blockchain >  SwiftUI: Filtered list indices are not corresponding to new list
SwiftUI: Filtered list indices are not corresponding to new list

Time:12-28

I have a Country model and then a JSON list of countries. I have creating the list by using a ForEach. If the search box is empty, the value of filteredCountries is just the whole list, otherwise the list only contains countries that include whats in the search box.
The problem is that when the list is filtered and I click to favorite an item, the wrong country is favorited. The indices don't match the filtered items, they still go in sequential order: 0,1,2,3 etc.

Country

import Foundation

struct Country: Codable, Identifiable {
    var id: String {image}
    let display_name: String
    let searchable_names: [String]
    let image: String
    var favorite: Bool
    var extended: Bool
}

ContentView

var filteredCountries : [Country] {
    if (searchText.isEmpty) {
        return countries;
    } else {
        return countries.filter { $0.display_name.contains(searchText) }
    }
}

          NavigationView {
                ScrollView {
                    ForEach(Array(filteredCountries.enumerated()), id: \.1.id) { (index,country) in
                        
                        LazyVStack {
                            ZStack(alignment: .bottom) {
                                Image("\(country.image)-bg")
                                    .resizable()
                                    .scaledToFit()
                                    .edgesIgnoringSafeArea(.all)
                                    .overlay(Rectangle().opacity(0.2))
                                    .mask(
                                        LinearGradient(gradient: Gradient(colors: [Color.black, Color.black.opacity(0)]), startPoint: .center, endPoint: .bottom)
                                    )
                                
                                
                                HStack {
                                    NavigationLink(
                                        destination: CountryView(country: country),
                                        label: {
                                            HStack {
                                                Image(country.image)
                                                    .resizable()
                                                    .frame(width: 50, height: 50)
                                                Text(country.display_name)
                                                    .foregroundColor(Color.black)
                                                    .padding(.leading)
                                                Spacer()
                                            }
                                            .padding(.top, 12.0)
                                            
                                        }
                                    ).buttonStyle(FlatLinkStyle())
                                    if country.favorite {
                                        Image(systemName: "heart.fill").foregroundColor(.red).onTapGesture {
                                            print(country)
                                            countries[index].favorite.toggle()
                                        }
                                        .padding(.top, 12)
                                    } else {
                                        Image(systemName: "heart").foregroundColor(.red).onTapGesture {
                                            print(country)

                                            countries[index].favorite.toggle()
                                        }
                                        .padding(.top, 12)
                                    }
                                    
                                }
                                .padding(.horizontal, 16.0)
                            }
                        }
                        .padding(.bottom, country.extended ? 220 : 0)
                        .onTapGesture {
                            withAnimation(.easeInOut(duration: 0.4)) {
                                self.countries[index].extended.toggle();
                                
                            }
                        }
                    }
                }
                .frame(maxWidth: bounds.size.width)
                .navigationTitle("Countries")
                .font(Font.custom("Avenir-Book", size: 28))


            }
            .navigationViewStyle(StackNavigationViewStyle())
            .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search Country")           

CodePudding user response:

I've made a simple demo you can try out. The most notable thing is filteredCountry being of type [Binding<Country>] - this allows us to mutate the element because it is a Binding. In the List/ForEach loop, use $country because we are passing in a Binding.

Code:

struct ContentView: View {
    @State private var countries: [Country] = ["Canada", "England", "France", "Germany", "US"]
    @State private var search = ""

    private var filteredCounties: [Binding<Country>] {
        if search.isEmpty {
            return $countries.map { $0 }
        } else {
            return $countries.filter { $country in
                country.name.lowercased().contains(search.lowercased())
            }
        }
    }

    var body: some View {
        NavigationView {
            List(filteredCounties) { $country in
                HStack {
                    Text(country.name)

                    Spacer()

                    Image(systemName: country.isFavorite ? "heart.fill" : "heart")
                        .foregroundColor(.red)
                        .onTapGesture {
                            country.isFavorite.toggle()
                        }
                }
            }
        }
        .navigationViewStyle(.stack)
        .searchable(text: $search, prompt: "Canada")
    }
}

The structure below of Country isn't so relevant, but is here for completeness:

struct Country: Identifiable, ExpressibleByStringLiteral {
    let id = UUID()
    let name: String
    var isFavorite = false

    init(stringLiteral value: StringLiteralType) {
        name = value
    }
}

Result:

Result

CodePudding user response:

in the ForEach(Array(filteredCountries.enumerated()), id: \.1.id) { (index,country) in ....

the index refers to the index of the "new" array consisting of just the filtered countries, not the original array of all the countries. To access the original array items, you could use the country id, such as:

    if let ndx = countries.firstIndex(where: {$0.id == country.id}) {
        countries[ndx].favorite.toggle()
     }
 

instead of:

 countries[index].favorite.toggle()
  • Related