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:
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()