UPD: I think i found a better solution to the problem, remove the if favorites.contains(item)
and in the List
add this code:
List(ViewModel.searchResults.filter({favorites.contains($0)}), id: \.idMeal)
Hi, i managed to make it work forcing a refresh with an id on the list here's the code:
MealListView:
struct MealListView: View {
@ObservedObject var ViewModel : MealListViewModel = MealListViewModel()
@StateObject var favorites = Favorites()
@State var id = UUID()
var body: some View {
if !ViewModel.showingFaves {
NavigationStack {
List(ViewModel.searchResults, id: \.idMeal) { item in
let foodImage = URL(string: item.strMealThumb)!
NavigationLink(destination: RecipeView(id: $id, currentMeal: item, mealID: item.idMeal).navigationTitle(item.strMeal).environmentObject(favorites)) {
HStack {
AsyncImage(url: foodImage, scale: 30.0){ image in image.resizable() } placeholder: { Color.gray } .frame(width: 75, height: 75) .clipShape(RoundedRectangle(cornerRadius: 10))
Text(item.strMeal)
Spacer()
Image(systemName: favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(Color.red)
}
}
}
.navigationTitle(Text("Choose a recipe!"))
.searchable(text: $ViewModel.searchString, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "Search for recipe...")
.task {
await ViewModel.loadList()
}
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button(ViewModel.buttonTitle) {
ViewModel.showingFaves.toggle()
}
}
}
}
}
else {
NavigationStack {
List(ViewModel.searchResults, id: \.idMeal) { item in
if favorites.contains(item) {
let foodImage = URL(string: item.strMealThumb)!
NavigationLink(destination: RecipeView(id: $id, currentMeal: item, mealID: item.idMeal).navigationTitle(item.strMeal).environmentObject(favorites)) {
HStack {
AsyncImage(url: foodImage, scale: 30.0){ image in image.resizable() } placeholder: { Color.gray } .frame(width: 75, height: 75) .clipShape(RoundedRectangle(cornerRadius: 10))
Text(item.strMeal)
Spacer()
Image(systemName: "heart.fill")
.foregroundColor(Color.red)
}
}
}
}
.id(id)
.navigationTitle(Text("Choose a recipe!"))
.searchable(text: $ViewModel.searchString, placement: .navigationBarDrawer(displayMode: .automatic), prompt: "Search for recipe...")
.task {
await ViewModel.loadList()
}
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button(ViewModel.buttonTitle) {
ViewModel.showingFaves.toggle()
}
}
}
}
.onAppear {
favorites.load()
}
}
}
}
RecipeView:
struct RecipeView: View {
@State private var recipeSteps = [String : String?]()
@EnvironmentObject var favorites : Favorites
@Binding var id: UUID
let currentMeal : MealEntry
let mealID : String
var body: some View {
let imageURL = "\((recipeSteps["strSource"] ?? "unknown source")!)"
let ingredients = extractIngredients().sorted(by: { $0.key < $1.key} )
GeometryReader { g in
List {
Section {
VStack {
HStack {
Spacer()
AsyncImage(url: URL(string: (recipeSteps["strMealThumb"] ?? "https://htmlcolorcodes.com/assets/images/colors/gray-color-solid-background-1920x1080.png")!)!, scale: 50.0) { image in image.resizable() } placeholder: { Color.gray }
.frame(width: g.size.width * 0.7633587786, height: g.size.width * 0.7633587786) .clipShape(RoundedRectangle(cornerRadius: 10))
Spacer()
}
Text("A delicious \((recipeSteps["strArea"] ?? "loading")!) \((recipeSteps["strCategory"] ?? "loading")!.lowercased())!")
.font(.subheadline)
Text("Tags: \((recipeSteps["strTags"] ?? "N/A")!)")
.font(.caption)
Link("Tap here for recipe video!", destination: URL(string: (recipeSteps["strYoutube"] ?? "loading")!)!)
}
}
Section {
HStack{
Spacer()
Button(favorites.contains(currentMeal) ? "Remove from Favorites" : "Add to Favorites") {
if favorites.contains(currentMeal) {
favorites.remove(currentMeal)
id = UUID()
} else {
favorites.add(currentMeal)
id = UUID()
}
}
Spacer()
}
}
Section(header: Text("You will need:")) {
ForEach(ingredients, id: \.key) { ingredient, amount in
Text("**\(ingredient)**: \(amount)")
}
}
.headerProminence(.increased)
Section {
Text((recipeSteps["strInstructions"] ?? "loading")!)
.textSelection(.enabled)
} header: {
Text("Recipe:")
.headerProminence(.increased)
} footer: {
Text("Recipe courtesy of \(try! AttributedString(markdown: imageURL))")
.font(.caption)
}
}
.onDisappear {
favorites.save()
}
.task {
await loadRecipe()
}
}
}
func loadRecipe() async {
guard let url = URL(string: "https://themealdb.com/api/json/v1/1/lookup.php?i=\(mealID)") else {
print("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let decodedResponse = try? JSONDecoder().decode(Recipe.self, from: data) {
let rawRecipe = decodedResponse.meals.first!
let cleanedRecipe = ((rawRecipe.compactMapValues({ $0 })).filter( { !$0.value.isEmpty })).filter( { !($0.value == " ") })
recipeSteps = cleanedRecipe
}
} catch {
print("Invalid data")
}
}
func extractIngredients() -> [String : String] {
var blankDict = [String : String]()
for i in 1...20 {
if recipeSteps.keys.contains("strIngredient\(i)") {
blankDict[recipeSteps["strIngredient\(i)"]!!.capitalized] = (recipeSteps["strMeasure\(i)"])!!
}
else {
return blankDict
}
}
return blankDict
}
func returnMeal() -> MealEntry {
return currentMeal
}
}