diff --git a/EatHub/EatHub/Application/AppDependencies.swift b/EatHub/EatHub/Application/AppDependencies.swift index 7c6e0e9..de54322 100644 --- a/EatHub/EatHub/Application/AppDependencies.swift +++ b/EatHub/EatHub/Application/AppDependencies.swift @@ -39,6 +39,6 @@ struct AppDependencies { } func makeFavoriteViewModel() -> FavoriteViewModel { - FavoriteViewModel() + FavoriteViewModel(favoritesManager: FavoritesManager(), mealsService: mealsService) } } diff --git a/EatHub/EatHub/Design/Extensions/UserDefaults+KeyValueStore.swift b/EatHub/EatHub/Design/Extensions/UserDefaults+KeyValueStore.swift new file mode 100644 index 0000000..e2be8fb --- /dev/null +++ b/EatHub/EatHub/Design/Extensions/UserDefaults+KeyValueStore.swift @@ -0,0 +1,13 @@ +// +// UserDefaults+KeyValueStore.swift +// EatHub +// +// Created by Stepan Chuiko on 30.03.2025. +// +import Foundation + +extension UserDefaults: KeyValueStore { + func array(forKey defaultName: String) -> [T] { + (self.array(forKey: defaultName) as? [T]) ?? [] + } +} diff --git a/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift b/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift index 52c2213..232a72d 100644 --- a/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift +++ b/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift @@ -38,3 +38,9 @@ extension MealItemResponseModel { ) } } + +extension Meal { + func mapToRecipe() -> RecipeViewModel { + RecipeViewModel(id: id, name: name, imageName: thumbnail ?? "MealTemplate") + } +} diff --git a/EatHub/EatHub/Modules/Favorite/FavoriteView.swift b/EatHub/EatHub/Modules/Favorite/FavoriteView.swift index fad17a1..c4d67bf 100644 --- a/EatHub/EatHub/Modules/Favorite/FavoriteView.swift +++ b/EatHub/EatHub/Modules/Favorite/FavoriteView.swift @@ -13,6 +13,9 @@ struct FavoriteView: View { } .background(Color(.systemGroupedBackground)) .navigationBarHidden(true) + .onAppear { + viewModel.refreshFavorites() + } } } } @@ -27,16 +30,16 @@ private extension FavoriteView { var favoritesList: some View { LazyVStack(spacing: 8) { - ForEach(viewModel.recipes) { recipe in - // TODO: Добавить переход - // NavigationLink(destination: RecipeDetailView(recipe: recipe)) { + ForEach(viewModel.likedRecipes) { recipe in + // TODO: Добавить переход + // NavigationLink(destination: RecipeDetailView(recipe: recipe)) { RecipeRow( recipe: recipe, onToggleFavorite: { viewModel.toggleFavorite(for: recipe) } ) - // } + // } .buttonStyle(PlainButtonStyle()) } } @@ -44,8 +47,3 @@ private extension FavoriteView { .padding(.top, 8) } } - -#Preview { - let viewModel = FavoriteViewModel() - FavoriteView(viewModel: viewModel) -} diff --git a/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift b/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift index 0adc1ea..b14ae46 100644 --- a/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift +++ b/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift @@ -1,32 +1,59 @@ import Foundation +import Combine + +final class FavoriteViewModel: ObservableObject { + @Published var likedRecipes: [RecipeViewModel] = [] + var recipesIdentifiers: [String] = [] -class FavoriteViewModel: ObservableObject { - @Published var recipes: [Recipe] = [] let title = "Favourites" - init() { - loadMockData() + private let favoritesManager: FavoritesManagerInterface + private let mealsService: MealsServiceInterface + private var cancellables = Set() + + init(favoritesManager: FavoritesManagerInterface, mealsService: MealsServiceInterface) { + self.favoritesManager = favoritesManager + self.mealsService = mealsService + + // убрать + favoritesManager.populateInitialFavorites(with: ["52943", "52869", "52883", "52823"]) } - func toggleFavorite(for recipe: Recipe) { - guard let index = recipes.firstIndex(of: recipe) else { return } - recipes[index].isFavorite.toggle() + func toggleFavorite(for recipe: RecipeViewModel) { + if recipe.isFavorite { + favoritesManager.remove(recipeID: recipe.id) + } else { + favoritesManager.add(recipeID: recipe.id) + } + + recipe.isFavorite.toggle() } - private func loadMockData() { - recipes = [ - Recipe(id: 0, name: "Паста Карбонара", imageName: "caesar"), - Recipe(id: 1, name: "Пицца Маргарита", imageName: "caesar"), - Recipe(id: 2, name: "Салат Цезарь", imageName: "caesar"), - Recipe(id: 3, name: "Паста Карбонара", imageName: "caesar"), - Recipe(id: 4, name: "Пицца Маргарита", imageName: "caesar"), - Recipe(id: 5, name: "Салат Цезарь", imageName: "caesar"), - Recipe(id: 6, name: "Паста Карбонара", imageName: "caesar"), - Recipe(id: 7, name: "Пицца Маргарита", imageName: "caesar"), - Recipe(id: 8, name: "Салат Цезарь", imageName: "caesar"), - Recipe(id: 9, name: "Паста Карбонара", imageName: "caesar"), - Recipe(id: 10, name: "Пицца Маргарита", imageName: "caesar"), - Recipe(id: 11, name: "Салат Цезарь", imageName: "caesar") - ] + func refreshFavorites() { + cancellables.removeAll() + + let currentFavoriteIDs = Set(favoritesManager.allFavorites()) + + likedRecipes.removeAll { !currentFavoriteIDs.contains($0.id) } + + let existingIDs = Set(likedRecipes.map { $0.id }) + let newIDs = currentFavoriteIDs.subtracting(existingIDs) + + loadNewRecipes(ids: Array(newIDs)) + } + + private func loadNewRecipes(ids: [String]) { + cancellables.removeAll() + + for id in ids { + mealsService.fetchMeal(id: id) + .receive(on: DispatchQueue.main) + .sink { _ in } receiveValue: { [weak self] meal in + if let recipe = meal?.mapToRecipe() { + self?.likedRecipes.append(recipe) + } + } + .store(in: &cancellables) + } } } diff --git a/EatHub/EatHub/Modules/Favorite/Models/Recipe.swift b/EatHub/EatHub/Modules/Favorite/Models/Recipe.swift deleted file mode 100644 index b93dd8b..0000000 --- a/EatHub/EatHub/Modules/Favorite/Models/Recipe.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -struct Recipe: Identifiable, Equatable { - let id: Int - var name: String - var imageName: String - var isFavorite: Bool = true -} diff --git a/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift b/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift new file mode 100644 index 0000000..1a535d7 --- /dev/null +++ b/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift @@ -0,0 +1,14 @@ +import Foundation + +class RecipeViewModel: ObservableObject, Identifiable { + let id: String + @Published var name: String + @Published var imageName: String + @Published var isFavorite: Bool = true + + init(id: String, name: String, imageName: String) { + self.id = id + self.name = name + self.imageName = imageName + } +} diff --git a/EatHub/EatHub/Modules/Favorite/RecipeRow.swift b/EatHub/EatHub/Modules/Favorite/RecipeRow.swift index f9a635d..4d8794a 100644 --- a/EatHub/EatHub/Modules/Favorite/RecipeRow.swift +++ b/EatHub/EatHub/Modules/Favorite/RecipeRow.swift @@ -1,7 +1,7 @@ import SwiftUI struct RecipeRow: View { - let recipe: Recipe + @ObservedObject var recipe: RecipeViewModel let onToggleFavorite: () -> Void private enum Constants { @@ -14,12 +14,19 @@ struct RecipeRow: View { var body: some View { HStack { - Image(recipe.imageName) - .resizable() - .aspectRatio(contentMode: .fill) + if let url = URL(string: recipe.imageName) { + CachedAsyncImage(url: url) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + Color.gray.opacity(0.3) + .skeletonable(true) + } .frame(width: Constants.imageWidth, height: Constants.imageHeight) .clipped() .cornerRadius(Constants.imageCornerRadius) + } Text(recipe.name) .font(.headline) diff --git a/EatHub/EatHub/Services/FavoritesManager/FavoritesManager.swift b/EatHub/EatHub/Services/FavoritesManager/FavoritesManager.swift new file mode 100644 index 0000000..ee6d5ce --- /dev/null +++ b/EatHub/EatHub/Services/FavoritesManager/FavoritesManager.swift @@ -0,0 +1,49 @@ +// +// FavoritesManager.swift +// EatHub +// +// Created by Stepan Chuiko on 29.03.2025. +// +import Foundation + +final class FavoritesManager { + private let store: KeyValueStore + private let favoritesKey = "favorite_recipes_ids" + + init(store: KeyValueStore = UserDefaults.standard) { + self.store = store + } +} + +extension FavoritesManager: FavoritesManagerInterface { + func add(recipeID: String) { + var current = allFavorites() + guard !current.contains(recipeID) else { return } + current.append(recipeID) + save(current) + } + + func remove(recipeID: String) { + var current = allFavorites() + current.removeAll { $0 == recipeID } + save(current) + } + + func isFavorite(recipeID: String) -> Bool { + allFavorites().contains(recipeID) + } + + func allFavorites() -> [String] { + store.array(forKey: favoritesKey) + } + + func populateInitialFavorites(with ids: [String]) { + var current = Set(allFavorites()) + ids.forEach { current.insert($0) } + save(Array(current)) + } + + private func save(_ ids: [String]) { + store.set(ids, forKey: favoritesKey) + } +} diff --git a/EatHub/EatHub/Services/FavoritesManager/FavoritesManagerInterface.swift b/EatHub/EatHub/Services/FavoritesManager/FavoritesManagerInterface.swift new file mode 100644 index 0000000..4c4aa6f --- /dev/null +++ b/EatHub/EatHub/Services/FavoritesManager/FavoritesManagerInterface.swift @@ -0,0 +1,14 @@ +// +// FavoritesManagerInterface.swift +// EatHub +// +// Created by Stepan Chuiko on 29.03.2025. +// + +protocol FavoritesManagerInterface { + func add(recipeID: String) + func remove(recipeID: String) + func isFavorite(recipeID: String) -> Bool + func allFavorites() -> [String] + func populateInitialFavorites(with ids: [String]) +} diff --git a/EatHub/EatHub/Services/FavoritesManager/KeyValueStore.swift b/EatHub/EatHub/Services/FavoritesManager/KeyValueStore.swift new file mode 100644 index 0000000..928d2a6 --- /dev/null +++ b/EatHub/EatHub/Services/FavoritesManager/KeyValueStore.swift @@ -0,0 +1,11 @@ +// +// KeyValueStore.swift +// EatHub +// +// Created by Stepan Chuiko on 30.03.2025. +// + +protocol KeyValueStore { + func set(_ value: Any?, forKey defaultName: String) + func array(forKey defaultName: String) -> [T] +}