From f523955da03e9f817497fb197d8f859211651cdb Mon Sep 17 00:00:00 2001 From: CaptainSharky Date: Wed, 2 Apr 2025 13:52:32 +0300 Subject: [PATCH 1/2] =?UTF-8?q?#40:=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=BD=D0=B0=20=D0=94=D0=B5=D1=82=D0=B0=D0=BB=D0=BA?= =?UTF-8?q?=D1=83=20=D0=B8=D0=B7=20=D0=98=D0=B7=D0=B1=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EatHub/Application/AppDependencies.swift | 10 ++- .../Mappers/MealMapper/MealMapper.swift | 2 +- .../Modules/Favorite/FavoriteView.swift | 70 +++++++++++++++---- .../Modules/Favorite/FavoriteViewModel.swift | 8 ++- .../Favorite/Models/RecipeViewModel.swift | 8 ++- 5 files changed, 80 insertions(+), 18 deletions(-) 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..b7ce426 100644 --- a/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift +++ b/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift @@ -41,6 +41,6 @@ extension MealItemResponseModel { extension Meal { func mapToRecipe() -> RecipeViewModel { - RecipeViewModel(id: id, name: name, imageName: thumbnail ?? "MealTemplate") + RecipeViewModel(id: id, meal: self) } } diff --git a/EatHub/EatHub/Modules/Favorite/FavoriteView.swift b/EatHub/EatHub/Modules/Favorite/FavoriteView.swift index c4d67bf..ac41d3b 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 selectedMeal: Meal? + @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 meal = selectedMeal, showDetail { + openDetailsView(for: meal) } } - .background(Color(.systemGroupedBackground)) .navigationBarHidden(true) - .onAppear { - viewModel.refreshFavorites() - } } } } @@ -31,19 +49,47 @@ private extension FavoriteView { var favoritesList: some View { LazyVStack(spacing: 8) { ForEach(viewModel.likedRecipes) { recipe in - // TODO: Добавить переход - // NavigationLink(destination: RecipeDetailView(recipe: recipe)) { RecipeRow( recipe: recipe, onToggleFavorite: { viewModel.toggleFavorite(for: recipe) } ) - // } - .buttonStyle(PlainButtonStyle()) + .onTapGesture { + withAnimation(.easeInOut(duration: Constants.animationDuration)) { + selectedMeal = recipe.meal + showDetail = true + } + } } } .padding(.horizontal) .padding(.top, 8) } + + @ViewBuilder + func openDetailsView(for meal: Meal) -> some View { + let viewModel = viewModel.detailsViewModelBuilder( + DetailsViewModuleInput( + id: meal.id, + name: meal.name, + thumbnail: meal.thumbnail + ) + ) + DetailsView( + viewModel: viewModel, + onClose: { + withAnimation(.easeInOut(duration: Constants.animationDuration)) { + showDetail = false + viewModel.isCloseButtonHidden = true + } + DispatchQueue.main.asyncAfter(deadline: .now() + Constants.animationDuration) { + selectedMeal = 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..7bd31f9 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"]) diff --git a/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift b/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift index 1a535d7..60994d1 100644 --- a/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift +++ b/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift @@ -2,13 +2,15 @@ import Foundation class RecipeViewModel: ObservableObject, Identifiable { let id: String + let meal: Meal @Published var name: String @Published var imageName: String @Published var isFavorite: Bool = true - init(id: String, name: String, imageName: String) { + init(id: String, meal: Meal) { self.id = id - self.name = name - self.imageName = imageName + self.name = meal.name + self.imageName = meal.thumbnail ?? "" + self.meal = meal } } From a0f78cba165c855c7830b454f6696eaec726871f Mon Sep 17 00:00:00 2001 From: CaptainSharky Date: Wed, 2 Apr 2025 14:09:34 +0300 Subject: [PATCH 2/2] =?UTF-8?q?#40:=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= =?UTF-8?q?=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Mappers/MealMapper/MealMapper.swift | 6 ----- .../Modules/Favorite/FavoriteView.swift | 24 +++++++++---------- .../Modules/Favorite/FavoriteViewModel.swift | 17 +++++++++---- .../Favorite/Models/RecipeViewModel.swift | 18 ++++++++------ .../EatHub/Modules/Favorite/RecipeRow.swift | 2 +- 5 files changed, 37 insertions(+), 30 deletions(-) diff --git a/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift b/EatHub/EatHub/Mappers/MealMapper/MealMapper.swift index b7ce426..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, meal: self) - } -} diff --git a/EatHub/EatHub/Modules/Favorite/FavoriteView.swift b/EatHub/EatHub/Modules/Favorite/FavoriteView.swift index ac41d3b..4fae335 100644 --- a/EatHub/EatHub/Modules/Favorite/FavoriteView.swift +++ b/EatHub/EatHub/Modules/Favorite/FavoriteView.swift @@ -2,7 +2,7 @@ import SwiftUI struct FavoriteView: View { @ObservedObject var viewModel: FavoriteViewModel - @State private var selectedMeal: Meal? + @State private var selectedItem: RecipeViewModel? @State private var showDetail: Bool = false @Namespace private var animationNamespace @@ -29,8 +29,8 @@ struct FavoriteView: View { viewModel.refreshFavorites() } - if let meal = selectedMeal, showDetail { - openDetailsView(for: meal) + if let selectedItem, showDetail { + openDetailsView(for: selectedItem) } } .navigationBarHidden(true) @@ -48,16 +48,16 @@ private extension FavoriteView { var favoritesList: some View { LazyVStack(spacing: 8) { - ForEach(viewModel.likedRecipes) { recipe in + ForEach(viewModel.likedRecipes) { recipeViewModel in RecipeRow( - recipe: recipe, + recipe: recipeViewModel, onToggleFavorite: { - viewModel.toggleFavorite(for: recipe) + viewModel.toggleFavorite(for: recipeViewModel) } ) .onTapGesture { withAnimation(.easeInOut(duration: Constants.animationDuration)) { - selectedMeal = recipe.meal + selectedItem = recipeViewModel showDetail = true } } @@ -68,12 +68,12 @@ private extension FavoriteView { } @ViewBuilder - func openDetailsView(for meal: Meal) -> some View { + func openDetailsView(for recipeViewModel: RecipeViewModel) -> some View { let viewModel = viewModel.detailsViewModelBuilder( DetailsViewModuleInput( - id: meal.id, - name: meal.name, - thumbnail: meal.thumbnail + id: recipeViewModel.id, + name: recipeViewModel.name, + thumbnail: recipeViewModel.thumbnail ) ) DetailsView( @@ -84,7 +84,7 @@ private extension FavoriteView { viewModel.isCloseButtonHidden = true } DispatchQueue.main.asyncAfter(deadline: .now() + Constants.animationDuration) { - selectedMeal = nil + selectedItem = nil } }, namespace: animationNamespace diff --git a/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift b/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift index 7bd31f9..deb8678 100644 --- a/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift +++ b/EatHub/EatHub/Modules/Favorite/FavoriteViewModel.swift @@ -54,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 60994d1..de67fc0 100644 --- a/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift +++ b/EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift @@ -2,15 +2,19 @@ import Foundation class RecipeViewModel: ObservableObject, Identifiable { let id: String - let meal: Meal @Published var name: String - @Published var imageName: String - @Published var isFavorite: Bool = true + @Published var thumbnail: String? + @Published var isFavorite: Bool - init(id: String, meal: Meal) { + init( + id: String, + name: String, + thumbnail: String?, + isFavorite: Bool = true + ) { self.id = id - self.name = meal.name - self.imageName = meal.thumbnail ?? "" - self.meal = meal + self.name = name + 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()