Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,56 @@
///
/// This model contains properties for the recipe title, recipe image URL, rating (out of 5 stars),
/// user image URL, user name, and preparation time.
struct Recipe {
public struct Recipe {

// MARK: - Properties

/// Recipe ID
public let recipeId: String

/// The title of the recipe
let title: String?
public let title: String?

/// A URL string representing the recipe image
let recipeImageUrl: String?
public let recipeImageUrl: String?

/// The rating of the recipe in the form of a star rating (out of 5 stars)
let rating: Int?
public let rating: Int?

/// A URL string representing the user who posted the recipe
let userImageUrl: String?
public let userImageUrl: String?

/// The name of the user who posted the recipe
let userName: String?
public let userName: String?

/// The preparation time for the recipe in seconds
let preparationTime: Int?
public let preparationTime: Int?

// MARK: - Initializers

/// Initializes a new instance of `Recipe` with the given parameters.
///
/// - Parameters:
/// - recipeId: The recipe Id
/// - title: The title of the recipe.
/// - recipeImageUrl: A URL string representing the recipe image.
/// - rating: The star rating of the recipe (out of 5 stars).
/// - userImageUrl: A URL string representing the user who posted the recipe.
/// - userName: The name of the user who posted the recipe.
/// - preparationTime: The preparation time for the recipe in minutes.
init(title: String = "",
recipeImageUrl: String = "",
rating: Int = 0,
userImageUrl: String = "",
userName: String = "",
preparationTime: Int = 0) {
public init(recipeId: String,
title: String = "",
recipeImageUrl: String = "",
rating: Int = 0,
userImageUrl: String = "",
userName: String = "",
preparationTime: Int = 0) {
self.recipeId = recipeId
self.title = title
self.recipeImageUrl = recipeImageUrl
self.rating = rating
self.userImageUrl = userImageUrl
self.userName = userName
self.preparationTime = preparationTime
}

// MARK: - Methods

/// Formats the preparation time as a string in the "Minutes:Seconds" format.
///
/// - Returns: A string representing the preparation time formatted as "Minutes:Seconds".
func formattedPreparationTime() -> String {
let minutes = (preparationTime ?? 0) / 60
let seconds = (preparationTime ?? 0) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}
5 changes: 5 additions & 0 deletions Domain/Sources/Domain/UseCases/FavoriteRecipe.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public protocol FavoriteRecipeUseCase {
func favoriteRecipe(_ recipe: Recipe)
func unfavoriteRecipe(_ recipe: Recipe)
func allFavoriteRecipes() -> [Recipe]
}
56 changes: 31 additions & 25 deletions Healthy.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Healthy/Classes/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import UIKit
final class AppCoordinator {
private let window: UIWindow
private var children: [Coordinator] = []
private var isLoggedIn = false // TODO: [HL-74] Replace with actual implentation
private var isLoggedIn = true // TODO: [HL-74] Replace with actual implentation

/// Initializes a new `AppCoordinator` object with the specified window.
///
Expand Down
38 changes: 0 additions & 38 deletions Healthy/Classes/Entities/SavedRecipe.swift

This file was deleted.

48 changes: 24 additions & 24 deletions Healthy/Classes/Modules/Dashboard/DashboardViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import Combine
import Domain
import Foundation
import Factory

// MARK: DashboardViewModel

final class DashboardViewModel {
@Injected(\.favoriteRecipeUseCase) private var favoriteRecipeUseCase

@Published private var newRecipes: [NewRecipeViewModel] = []

init() {
Expand Down Expand Up @@ -31,31 +35,27 @@ extension DashboardViewModel: DashboardViewModelOutput {

private extension DashboardViewModel {
private func loadPlaceholderNewRecipes() {
newRecipes = [
NewRecipeViewModel(
recipeName: "",
userName: "",
preparationTimeInMinutes: "",
recipeImageUrl: "",
rating: .zero,
userImageUrl: nil
),
NewRecipeViewModel(
recipeName: "",
userName: "",
preparationTimeInMinutes: "",
recipeImageUrl: "",
rating: .zero,
userImageUrl: nil
),
let recipes = [
Recipe(recipeId: "first-recipe",
title: "First Recipe"),
Recipe(recipeId: "second-recipe",
title: "Second Recipe"),
Recipe(recipeId: "third-recipe",
title: "Third Recipe")
]

newRecipes = recipes.map { recipe in
NewRecipeViewModel(
recipeName: "",
userName: "",
preparationTimeInMinutes: "",
recipeImageUrl: "",
rating: .zero,
userImageUrl: nil
recipeName: recipe.title ?? String(),
userName: recipe.userName ?? String(),
preparationTimeInMinutes: recipe.preparationTime?.description ?? String(),
recipeImageUrl: recipe.recipeImageUrl,
rating: recipe.rating,
userImageUrl: recipe.userImageUrl,
onTap: { [weak self] in
self?.favoriteRecipeUseCase.favoriteRecipe(recipe)
}
)
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ final class NewRecipeCollectionViewCell: UICollectionViewCell {
@IBOutlet weak private(set) var preparationTimeHorizontalStackView: UIStackView!
@IBOutlet weak private(set) var cardView: UIView!

// MARK: - Properties

private var onTap: () -> Void = { }

// MARK: - Lifecycle

override func awakeFromNib() {
super.awakeFromNib()
configureAppearance()
configureOnTapGesture()
}

override func prepareForReuse() {
Expand All @@ -33,6 +38,12 @@ final class NewRecipeCollectionViewCell: UICollectionViewCell {
cardView.applyDefaultCardShadow(cornerRadius: Constants.cornerRadius)
}

private func configureOnTapGesture() {
addGestureRecognizer(
UITapGestureRecognizer(target: self, action: #selector(viewWasTapped))
)
}

/// Sets placeholder images for testing purposes
///
/// Use this till a `ViewModel` is passed and configures the view properly
Expand All @@ -41,6 +52,11 @@ final class NewRecipeCollectionViewCell: UICollectionViewCell {
recipeImage.image = UIImage.imageRecipePlaceholder
}

/// Called when the cell is tapped
@objc private func viewWasTapped() {
onTap()
}

/// Configures the cell view and binds the data from the provided view model.
///
/// - Parameter viewModel: The view model containing the data to be displayed in the cell view.
Expand All @@ -49,6 +65,7 @@ final class NewRecipeCollectionViewCell: UICollectionViewCell {
userNameLabel.text = viewModel.userName
preparationTimeInMinutesLabel.text = viewModel.preparationTimeInMinutes

onTap = viewModel.onTap
// TODO: [HL-52] To be Implemented after implementing binding imageUrl To UIImageView
}
}
Expand All @@ -70,7 +87,6 @@ extension NewRecipeCollectionViewCell {

/// The view model used to configure the `NewRecipeCollectionViewCell`.
struct ViewModel: Equatable {

/// The name of the recipe.
let recipeName: String

Expand All @@ -88,5 +104,13 @@ extension NewRecipeCollectionViewCell {

/// The image urlrepresenting the user photo.
let userImageUrl: String?

/// On cell tap action
let onTap: () -> Void

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.recipeName == rhs.recipeName
&& lhs.userName == rhs.userName
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ extension SavedRecipesTableViewCell {
}

static func == (lhs: SavedRecipesTableViewCell.ViewModel, rhs: SavedRecipesTableViewCell.ViewModel) -> Bool {
return lhs.title == rhs.title &&
lhs.rating == rhs.rating &&
lhs.chefName == rhs.chefName &&
lhs.cookingTime == rhs.cookingTime &&
lhs.recipeImage == rhs.recipeImage
return lhs.id == rhs.id
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ final class SavedRecipesViewController: UIViewController {
configureTableViewDataSource()
setupBindings()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

viewModel.viewWillAppear()
}
}

// MARK: - Actions
Expand Down
38 changes: 25 additions & 13 deletions Healthy/Classes/Modules/SavedRecipes/SavedRecipesViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
import Combine
import UIKit
import Factory

// MARK: SavedRecipesViewModel

final class SavedRecipesViewModel {

typealias CellViewModel = SavedRecipesTableViewCell.ViewModel

@Injected(\.favoriteRecipeUseCase) private var favoriteRecipeUseCase

// MARK: - Properties

@Published private (set) var savedRecipes: [SavedRecipesTableViewCell.ViewModel] = [
SavedRecipesTableViewCell.ViewModel(title: "Traditional spare ribs baked ",
recipeImage: UIImage.iconFood,
rating: 4.5,
chefName: "By Chef John",
cookingTime: 15, toggleBookmark: {}),
SavedRecipesTableViewCell.ViewModel(title: "spice roasted chicken with flavored rice",
recipeImage: UIImage.iconFood,
rating: 5.0,
chefName: "By Mark Kelvin",
cookingTime: 20, toggleBookmark: {})
]
@Published private(set) var savedRecipes: [CellViewModel] = []
}

// MARK: SavedRecipesViewModel
Expand All @@ -29,6 +23,10 @@ extension SavedRecipesViewModel: SavedRecipesViewModelInput {
savedRecipes.remove(at: index)
}
}

func viewWillAppear() {
reloadSavedRecipes()
}
}

// MARK: SavedRecipesViewModelOutput
Expand All @@ -41,4 +39,18 @@ extension SavedRecipesViewModel: SavedRecipesViewModelOutput {

// MARK: Private Handlers

private extension SavedRecipesViewModel {}
private extension SavedRecipesViewModel {
private func reloadSavedRecipes() {
let recipes = favoriteRecipeUseCase.allFavoriteRecipes()
savedRecipes = recipes.map { recipe in
CellViewModel(title: recipe.title,
recipeImage: UIImage.iconFood,
rating: Double(recipe.rating ?? .zero),
chefName: recipe.userName,
cookingTime: recipe.preparationTime) { [weak self] in
self?.favoriteRecipeUseCase.unfavoriteRecipe(recipe)
self?.reloadSavedRecipes()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ typealias SavedRecipesViewModelType = SavedRecipesViewModelInput & SavedRecipesV
///
protocol SavedRecipesViewModelInput {
func removeSavedRecipe(_ recipe: SavedRecipesTableViewCell.ViewModel)
func viewWillAppear()
}

/// SavedRecipes ViewModel Output
Expand Down
13 changes: 13 additions & 0 deletions Healthy/Classes/Modules/Search/Search/SearchViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import Combine
import Domain

protocol SearchDataSource {
func loadRecipes() async throws -> [Recipe]
Expand Down Expand Up @@ -129,3 +130,15 @@ private extension SearchViewModel {
}
}
}

// MARK: - Recipe Helpers
private extension Recipe {
/// Formats the preparation time as a string in the "Minutes:Seconds" format.
///
/// - Returns: A string representing the preparation time formatted as "Minutes:Seconds".
func formattedPreparationTime() -> String {
let minutes = (preparationTime ?? 0) / 60
let seconds = (preparationTime ?? 0) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Combine
import Domain

/// Search Input & Output
///
Expand Down
Loading