Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion EatHub/EatHub/Domain/Meal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Kirill Prokofyev on 25.03.2025.
//

struct Meal: Identifiable, Equatable {
struct Meal: Identifiable, Equatable, Hashable {
let id: String
let name: String
let category: String?
Expand Down
102 changes: 44 additions & 58 deletions EatHub/EatHub/Modules/Details/DetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SwiftUI
struct DetailsView: View {
private enum Constants {
static let chipSpacing: CGFloat = 8
static let closeButtonSize: CGFloat = 44
static let backButtonSize: CGFloat = 44
static let horizontalPadding: CGFloat = 16
static let imageCornerRadius: CGFloat = 24
static let imageHeight: CGFloat = 200
Expand All @@ -19,7 +19,7 @@ struct DetailsView: View {
enum Icons {
static let area: String = "globe"
static let category: String = "square.grid.2x2"
static let close: String = "xmark"
static let back: String = "arrow.backward"
static let youtube: String = "play.rectangle.fill"
}

Expand All @@ -31,45 +31,35 @@ struct DetailsView: View {
}

@ObservedObject var viewModel: DetailsViewModel
let onClose: (() -> Void)?
let namespace: Namespace.ID
@Environment(\.dismiss) private var dismiss

var body: some View {
Group {
ZStack {
ScrollView {
VStack(alignment: .leading, spacing: Constants.spacing) {
VerticalItemView(
viewModel: viewModel.verticalItemViewModel,
namespace: namespace
)
.matchedGeometryEffect(
id: MatchedGeometryEffectIdentifier(.info, for: viewModel.id),
in: namespace
)
VerticalItemView(viewModel: viewModel.verticalItemViewModel)
makeTagsChipsScrollView()
ingredientsSection
instructionsSection
}
.background(Color(.systemBackground))
}
.ignoresSafeArea(edges: .top)
.safeAreaInset(edge: .top) {
HStack {
makeBackButton()
Spacer()
makeLikeButton()
}
.padding(.horizontal, Constants.spacing)
.padding(.vertical, Constants.spacing)
}
}
.navigationBarHidden(true)
.background(Color(.systemBackground))
.onFirstAppear {
.onAppear {
viewModel.fetchMeal()
}
.overlay(
HStack(alignment: .top, spacing: .zero) {
makeLikeButton()
Spacer()
makeCloseButton()
}
.padding(.horizontal, Constants.spacing)
.padding(.top, Constants.spacing)
.transaction { transaction in
transaction.animation = nil
},
alignment: .top
)
}
}

Expand All @@ -95,7 +85,6 @@ private extension DetailsView {
Text(Constants.Title.ingredients)
.font(.headline)
.padding(.top)

ForEach(viewModel.ingredients, id: \.self) { ingredient in
HStack {
Text(ingredient.name)
Expand All @@ -117,6 +106,9 @@ private extension DetailsView {
if let instructions = viewModel.instructions {
VStack {
makeInstructionsContent(instructions: instructions)
if let url = viewModel.youtubeURL {
makeYoutubeLink(url: url)
}
}
.padding(Constants.spacing)
.frame(maxWidth: .infinity)
Expand All @@ -125,22 +117,26 @@ private extension DetailsView {
}

@ViewBuilder
func makeCloseButton() -> some View {
if !viewModel.isCloseButtonHidden {
Button(action: { onClose?() }) {
ZStack {
Circle()
.fill(Color.black.opacity(0.3))
.shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)

Image(systemName: Constants.Icons.close)
.font(.title)
.symbolVariant(viewModel.isLiked ? .fill : .none)
.padding(12)
.foregroundColor(.white)
}
.frame(width: Constants.closeButtonSize, height: Constants.closeButtonSize)
func makeBackButton() -> some View {
Button(action: { dismiss() }) {
ZStack {
Circle()
.fill(Color.black.opacity(0.3))
.shadow(
color: Color.black.opacity(0.1),
radius: 4,
x: .zero,
y: 2
)
Image(systemName: Constants.Icons.back)
.font(.title)
.padding(12)
.foregroundColor(.white)
}
.frame(
width: Constants.backButtonSize,
height: Constants.backButtonSize
)
}
}

Expand All @@ -160,19 +156,13 @@ private extension DetailsView {
VStack(alignment: .leading, spacing: Constants.spacing) {
Text(Constants.Title.instructions)
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)

.frame(alignment: .leading)
Text(instructions)
.font(.body)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)

if let url = viewModel.youtubeURL {
makeYoutubeLink(url: url)
}
.frame(alignment: .leading)
}
.padding(Constants.spacing)
.frame(maxWidth: .infinity)
.background(Color.secondary.opacity(0.1))
.cornerRadius(Constants.spacing)
}
Expand All @@ -185,10 +175,9 @@ private extension DetailsView {
Text(Constants.Title.youtube)
.bold()
}
.frame(maxWidth: .infinity)
.padding(Constants.spacing)
.foregroundColor(.accent)
.background(Color.white)
.foregroundColor(.white)
.background(Color.accentColor)
.cornerRadius(Constants.spacing)
}
}
Expand All @@ -197,7 +186,6 @@ private extension DetailsView {
// MARK: - Preview

#Preview("Meal Details") {
@Previewable @Namespace var namespace
let requester = APIRequester()
let mealsService = MealsService(requester: requester)
let favoritesManager = FavoritesManager(store: UserDefaults.standard)
Expand Down Expand Up @@ -228,9 +216,7 @@ private extension DetailsView {
youtubeURL: URL(string: "example.com"),
favoritesManager: favoritesManager,
mealsService: mealsService
),
onClose: nil,
namespace: namespace
)
)
}
}
6 changes: 3 additions & 3 deletions EatHub/EatHub/Modules/Details/DetailsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class DetailsViewModel: ObservableObject {
@Published var ingredients: [Ingredient]
@Published var youtubeURL: URL?

@Published var isCloseButtonHidden: Bool
@Published var isBackButtonHidden: Bool
@Published var isSkeletonable: Bool

var isLiked: Bool {
Expand Down Expand Up @@ -53,7 +53,7 @@ final class DetailsViewModel: ObservableObject {
youtubeURL: URL? = nil,
favoritesManager: FavoritesManagerInterface,
mealsService: MealsServiceInterface,
isCloseButtonHidden: Bool = false,
isBackButtonHidden: Bool = false,
isSkeletonable: Bool = true
) {
self.id = id
Expand All @@ -67,7 +67,7 @@ final class DetailsViewModel: ObservableObject {
self.youtubeURL = youtubeURL
self.favoritesManager = favoritesManager
self.mealsService = mealsService
self.isCloseButtonHidden = isCloseButtonHidden
self.isBackButtonHidden = isBackButtonHidden
self.isSkeletonable = isSkeletonable
}

Expand Down
73 changes: 23 additions & 50 deletions EatHub/EatHub/Modules/Favorite/FavoriteView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,23 @@ struct FavoriteView: View {
}

var body: some View {
NavigationView {
ZStack {
contentView
.background(Color(.systemGroupedBackground))
.onAppear {
viewModel.refreshFavorites()
}

if let selectedItem, showDetail {
openDetailsView(for: selectedItem)
NavigationStack {
contentView
.background(Color(.systemGroupedBackground))
.onAppear {
viewModel.refreshFavorites()
}
}
.navigationBarHidden(true)
}
.navigationDestination(for: RecipeViewModel.self) { recipeViewModel in
let input = DetailsViewModuleInput(
id: recipeViewModel.id,
name: recipeViewModel.name,
thumbnail: recipeViewModel.thumbnail
)
let detailsViewModel = viewModel.detailsViewModelBuilder(input)
DetailsView(viewModel: detailsViewModel)
}
.navigationTitle(viewModel.title)
}
}

Expand Down Expand Up @@ -80,23 +83,20 @@ private extension FavoriteView {
var favoritesList: some View {
LazyVStack(spacing: 8) {
ForEach(viewModel.likedRecipes) { recipeViewModel in
RecipeRow(
recipe: recipeViewModel,
onToggleFavorite: {
viewModel.toggleFavorite(for: recipeViewModel)
}
)
.onTapGesture {
withAnimation(.easeInOut(duration: Constants.animationDuration)) {
selectedItem = recipeViewModel
showDetail = true
}
NavigationLink(value: recipeViewModel) {
RecipeRow(
recipe: recipeViewModel,
onToggleFavorite: {
viewModel.toggleFavorite(for: recipeViewModel)
}
)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(.top, 8)
}

var emptyPlaceholder: some View {
VStack(spacing: 16) {
Image(systemName: "heart")
Expand All @@ -114,31 +114,4 @@ private extension FavoriteView {
}
.frame(maxWidth: .infinity, alignment: .center)
}

@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
self.viewModel.refreshFavorites()
}
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.animationDuration) {
selectedItem = nil
}
},
namespace: animationNamespace
)
.zIndex(Constants.detailZIndex)
.transition(Constants.detailTransition)
}
}
12 changes: 12 additions & 0 deletions EatHub/EatHub/Modules/Favorite/Models/RecipeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,15 @@ class RecipeViewModel: ObservableObject, Identifiable {
self.isFavorite = isFavorite
}
}

extension RecipeViewModel: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}

extension RecipeViewModel: Equatable {
static func == (lhs: RecipeViewModel, rhs: RecipeViewModel) -> Bool {
lhs.id == rhs.id
}
}
Loading