diff --git a/EatHub/EatHub/Application/AppDependencies.swift b/EatHub/EatHub/Application/AppDependencies.swift index de54322..f98f533 100644 --- a/EatHub/EatHub/Application/AppDependencies.swift +++ b/EatHub/EatHub/Application/AppDependencies.swift @@ -12,12 +12,16 @@ struct AppDependencies { let mealsService: MealsService let detailsViewModelBuilder: (DetailsViewModuleInput) -> DetailsViewModel let launchScreenStateManager: LaunchScreenStateManager + let favoritesManager: FavoritesManagerInterface init() { let apiRequester = APIRequester() let mealsService = MealsService(requester: apiRequester) self.mealsService = mealsService + let favoritesManager = FavoritesManager() + self.favoritesManager = favoritesManager + let detailsViewModelBuilder: ((DetailsViewModuleInput) -> DetailsViewModel) = { input in DetailsViewModel( id: input.id, @@ -39,6 +43,10 @@ struct AppDependencies { } func makeFavoriteViewModel() -> FavoriteViewModel { - FavoriteViewModel(favoritesManager: FavoritesManager(), mealsService: mealsService) + FavoriteViewModel( + favoritesManager: favoritesManager, + mealsService: mealsService, + detailsViewModelBuilder: detailsViewModelBuilder + ) } } diff --git a/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift b/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift index 232a72d..52c2213 100644 --- a/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift +++ b/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift @@ -38,9 +38,3 @@ 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 c4d67bf..4fae335 100644 --- a/EatHub/EatHub/Modules/Favorite/FavoriteView.swift +++ b/EatHub/EatHub/Modules/Favorite/FavoriteView.swift @@ -2,20 +2,38 @@ import SwiftUI struct FavoriteView: View { @ObservedObject var viewModel: FavoriteViewModel + @State private var selectedItem: RecipeViewModel? + @State private var showDetail: Bool = false + @Namespace private var animationNamespace + + private enum Constants { + static let animationDuration: TimeInterval = 0.5 + static let detailZIndex: Double = 1 + static let detailTransition: AnyTransition = .asymmetric( + insertion: .move(edge: .bottom), + removal: .move(edge: .bottom) + ) + } var body: some View { NavigationView { - ScrollView { - VStack(alignment: .leading, spacing: 0) { - favoritesTitle - favoritesList + ZStack { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + favoritesTitle + favoritesList + } + } + .background(Color(.systemGroupedBackground)) + .onAppear { + viewModel.refreshFavorites() + } + + if let selectedItem, showDetail { + openDetailsView(for: selectedItem) } } - .background(Color(.systemGroupedBackground)) .navigationBarHidden(true) - .onAppear { - viewModel.refreshFavorites() - } } } } @@ -30,20 +48,48 @@ private extension FavoriteView { var favoritesList: some View { LazyVStack(spacing: 8) { - ForEach(viewModel.likedRecipes) { recipe in - // TODO: Добавить переход - // NavigationLink(destination: RecipeDetailView(recipe: recipe)) { + ForEach(viewModel.likedRecipes) { recipeViewModel in RecipeRow( - recipe: recipe, + recipe: recipeViewModel, onToggleFavorite: { - viewModel.toggleFavorite(for: recipe) + viewModel.toggleFavorite(for: recipeViewModel) } ) - // } - .buttonStyle(PlainButtonStyle()) + .onTapGesture { + withAnimation(.easeInOut(duration: Constants.animationDuration)) { + selectedItem = recipeViewModel + showDetail = true + } + } } } .padding(.horizontal) .padding(.top, 8) } + + @ViewBuilder + func openDetailsView(for recipeViewModel: RecipeViewModel) -> some View { + let viewModel = viewModel.detailsViewModelBuilder( + DetailsViewModuleInput( + id: recipeViewModel.id, + name: recipeViewModel.name, + thumbnail: recipeViewModel.thumbnail + ) + ) + DetailsView( + viewModel: viewModel, + onClose: { + withAnimation(.easeInOut(duration: Constants.animationDuration)) { + showDetail = false + viewModel.isCloseButtonHidden = true + } + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.animationDuration) { + selectedItem = nil + } + }, + namespace: animationNamespace + ) + .zIndex(Constants.detailZIndex) + .transition(Constants.detailTransition) + } } diff --git a/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift b/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift index b14ae46..deb8678 100644 --- a/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift +++ b/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift @@ -4,6 +4,7 @@ import Combine final class FavoriteViewModel: ObservableObject { @Published var likedRecipes: [RecipeViewModel] = [] var recipesIdentifiers: [String] = [] + var detailsViewModelBuilder: (DetailsViewModuleInput) -> DetailsViewModel let title = "Favourites" @@ -11,9 +12,14 @@ final class FavoriteViewModel: ObservableObject { private let mealsService: MealsServiceInterface private var cancellables = Set() - init(favoritesManager: FavoritesManagerInterface, mealsService: MealsServiceInterface) { + init( + favoritesManager: FavoritesManagerInterface, + mealsService: MealsServiceInterface, + detailsViewModelBuilder: @escaping ((DetailsViewModuleInput) -> DetailsViewModel) + ) { self.favoritesManager = favoritesManager self.mealsService = mealsService + self.detailsViewModelBuilder = detailsViewModelBuilder // убрать favoritesManager.populateInitialFavorites(with: ["52943", "52869", "52883", "52823"]) @@ -48,11 +54,20 @@ final class FavoriteViewModel: ObservableObject { 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) + .sink( + receiveCompletion: { _ in }, + receiveValue: { [weak self] meal in + guard let self else { return } + guard let meal else { return } + + let recipeRowViewModel = RecipeViewModel( + id: meal.id, + name: meal.name, + thumbnail: meal.thumbnail + ) + likedRecipes.append(recipeRowViewModel) } - } + ) .store(in: &cancellables) } } diff --git a/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift b/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift index 1a535d7..de67fc0 100644 --- a/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift +++ b/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift @@ -3,12 +3,18 @@ import Foundation class RecipeViewModel: ObservableObject, Identifiable { let id: String @Published var name: String - @Published var imageName: String - @Published var isFavorite: Bool = true + @Published var thumbnail: String? + @Published var isFavorite: Bool - init(id: String, name: String, imageName: String) { + init( + id: String, + name: String, + thumbnail: String?, + isFavorite: Bool = true + ) { self.id = id self.name = name - self.imageName = imageName + self.thumbnail = thumbnail + self.isFavorite = isFavorite } } diff --git a/EatHub/EatHub/Modules/Favorite/RecipeRow.swift b/EatHub/EatHub/Modules/Favorite/RecipeRow.swift index 4d8794a..aa9526f 100644 --- a/EatHub/EatHub/Modules/Favorite/RecipeRow.swift +++ b/EatHub/EatHub/Modules/Favorite/RecipeRow.swift @@ -14,7 +14,7 @@ struct RecipeRow: View { var body: some View { HStack { - if let url = URL(string: recipe.imageName) { + if let url = URL(string: recipe.thumbnail ?? "") { CachedAsyncImage(url: url) { image in image .resizable()