From 49293aaf9b8203baedee61b38ec1f40a60231f5e Mon Sep 17 00:00:00 2001 From: JoaoPinhoMinder Date: Thu, 6 Mar 2025 16:46:11 +0000 Subject: [PATCH 1/6] ALFMOB-149: Bag & Wishlist - Redirect to PDP In the **Bag** and **Wishlist** screens, tapping on a product cell now redirects the user to the **PDP** of the selected item, ensuring a smooth transition while preserving selected attributes like size and color. **Changes & Improvements:** - Made product cells tappable, including a tap effect for better user feedback. - Renamed `SelectionProduct` to `SelectedProduct` for better clarity. - Updated `SelectedProduct` to include `Product` and `Product.Variant` properties. - Implemented a mechanism to open **PDP** with a `SelectedProduct`. - Fixed an issue in `buildColorAndSizingSelectionConfigurations` where the `Product.Variant` parameter was not being properly used. --- ....swift => SelectedProduct+Extension.swift} | 2 +- Alfie/Alfie/Navigation/Coordinator.swift | 4 ++ Alfie/Alfie/Navigation/Screen.swift | 1 + Alfie/Alfie/Navigation/TabCoordinator.swift | 3 ++ Alfie/Alfie/Navigation/ViewFactory.swift | 17 ++++++ Alfie/Alfie/Views/BagView/BagView.swift | 10 +++- Alfie/Alfie/Views/BagView/BagViewModel.swift | 22 ++++---- .../ProductDetailsViewModel.swift | 44 ++++++++++----- .../ProductListingViewModel.swift | 11 ++-- .../Views/WishlistView/WishlistView.swift | 20 ++++--- .../WishlistView/WishlistViewModel.swift | 32 +++++------ .../Core/Services/Bag/BagService.swift | 8 +-- .../Services/Wishlist/WishlistService.swift | 10 ++-- .../Core/Features/MockBagViewModel.swift | 16 +++--- .../MockProductListingViewModel.swift | 4 +- .../Mocks/Core/Services/MockBagService.swift | 8 +-- .../Core/Services/MockWishlistService.swift | 10 ++-- .../Features/BagViewModelProtocol.swift | 6 +-- .../ProductListingViewModelProtocol.swift | 2 +- .../Features/WishlistViewModelProtocol.swift | 8 +-- .../Models/Models/Base/SelectedProduct.swift | 53 +++++++++++++++++++ .../Models/Models/Base/SelectionProduct.swift | 27 ---------- .../Services/Bag/BagServiceProtocol.swift | 6 +-- .../Wishlist/WishlistServiceProtocol.swift | 6 +-- 24 files changed, 208 insertions(+), 122 deletions(-) rename Alfie/Alfie/Extensions/{SelectionProduct+Extension.swift => SelectedProduct+Extension.swift} (95%) create mode 100644 Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift delete mode 100644 Alfie/AlfieKit/Sources/Models/Models/Base/SelectionProduct.swift diff --git a/Alfie/Alfie/Extensions/SelectionProduct+Extension.swift b/Alfie/Alfie/Extensions/SelectedProduct+Extension.swift similarity index 95% rename from Alfie/Alfie/Extensions/SelectionProduct+Extension.swift rename to Alfie/Alfie/Extensions/SelectedProduct+Extension.swift index e1d16a77..f07580f3 100644 --- a/Alfie/Alfie/Extensions/SelectionProduct+Extension.swift +++ b/Alfie/Alfie/Extensions/SelectedProduct+Extension.swift @@ -1,7 +1,7 @@ import Foundation import Models -extension SelectionProduct { +extension SelectedProduct { var sizeText: String { var sizeValue: String = "" if let size { diff --git a/Alfie/Alfie/Navigation/Coordinator.swift b/Alfie/Alfie/Navigation/Coordinator.swift index d85be9ef..a7199899 100644 --- a/Alfie/Alfie/Navigation/Coordinator.swift +++ b/Alfie/Alfie/Navigation/Coordinator.swift @@ -142,6 +142,10 @@ final class Coordinator: ObservableObject, CoordinatorProtocol { navigationAdapter.push(.productDetails(.product(product))) } + public func openDetails(for selectedProduct: SelectedProduct) { + navigationAdapter.push(.productDetails(.selectedProduct(selectedProduct))) + } + // MARK: - Brands public func openBrands() { diff --git a/Alfie/Alfie/Navigation/Screen.swift b/Alfie/Alfie/Navigation/Screen.swift index daa099e7..c5e19a4f 100644 --- a/Alfie/Alfie/Navigation/Screen.swift +++ b/Alfie/Alfie/Navigation/Screen.swift @@ -43,6 +43,7 @@ enum HomeTabConfig: Equatable, Hashable { enum ThemedProductDetailsScreen: Equatable, Hashable { case id(_ id: String) case product(_ product: Product) + case selectedProduct(_ selectedProduct: SelectedProduct) } struct ProductListingScreenConfiguration: Equatable, Hashable { diff --git a/Alfie/Alfie/Navigation/TabCoordinator.swift b/Alfie/Alfie/Navigation/TabCoordinator.swift index c4532c61..eb3c5e61 100644 --- a/Alfie/Alfie/Navigation/TabCoordinator.swift +++ b/Alfie/Alfie/Navigation/TabCoordinator.swift @@ -113,6 +113,9 @@ final class TabCoordinator: TabCoordinatorProtocol, ObservableObject { case .product(let product): coordinator.openDetails(for: product) + + case .selectedProduct(let selectedProduct): + coordinator.openDetails(for: selectedProduct) } case .categoryList(let categories, let title): diff --git a/Alfie/Alfie/Navigation/ViewFactory.swift b/Alfie/Alfie/Navigation/ViewFactory.swift index 10f41e66..49959ff2 100644 --- a/Alfie/Alfie/Navigation/ViewFactory.swift +++ b/Alfie/Alfie/Navigation/ViewFactory.swift @@ -168,6 +168,23 @@ final class ViewFactory: ViewFactoryProtocol { ) ) ) + + case .selectedProduct(let selectedProduct): + ProductDetailsView( + viewModel: ProductDetailsViewModel( + productId: selectedProduct.product.id, + product: selectedProduct.product, + selectedProduct: selectedProduct, + dependencies: ProductDetailsDependencyContainer( + productService: serviceProvider.productService, + webUrlProvider: serviceProvider.webUrlProvider, + bagService: serviceProvider.bagService, + wishlistService: serviceProvider.wishlistService, + configurationService: serviceProvider.configurationService, + analytics: serviceProvider.analytics + ) + ) + ) } case .recentSearches: diff --git a/Alfie/Alfie/Views/BagView/BagView.swift b/Alfie/Alfie/Views/BagView/BagView.swift index 91594c2d..4e839f6f 100644 --- a/Alfie/Alfie/Views/BagView/BagView.swift +++ b/Alfie/Alfie/Views/BagView/BagView.swift @@ -6,6 +6,7 @@ import Mocks #endif struct BagView: View { + @EnvironmentObject var coordinador: Coordinator @StateObject private var viewModel: ViewModel init(viewModel: ViewModel) { @@ -15,7 +16,14 @@ struct BagView: View { var body: some View { List { ForEach(viewModel.products) { product in - HorizontalProductCard(viewModel: viewModel.productCardViewModel(for: product)) + Button( + action: { coordinador.openDetails(for: product) }, + label: { + HorizontalProductCard(viewModel: viewModel.productCardViewModel(for: product)) + .contentShape(Rectangle()) + } + ) + .buttonStyle(.plain) .listRowInsets(EdgeInsets()) } .onDelete { offsets in diff --git a/Alfie/Alfie/Views/BagView/BagViewModel.swift b/Alfie/Alfie/Views/BagView/BagViewModel.swift index 90b558e9..676a39e2 100644 --- a/Alfie/Alfie/Views/BagView/BagViewModel.swift +++ b/Alfie/Alfie/Views/BagView/BagViewModel.swift @@ -3,7 +3,7 @@ import Models import SharedUI final class BagViewModel: BagViewModelProtocol { - @Published private(set) var products: [SelectionProduct] + @Published private(set) var products: [SelectedProduct] private let dependencies: BagDependencyContainer @@ -18,22 +18,22 @@ final class BagViewModel: BagViewModelProtocol { products = dependencies.bagService.getBagContent() } - func didSelectDelete(for product: SelectionProduct) { - dependencies.bagService.removeProduct(product) + func didSelectDelete(for selectedProduct: SelectedProduct) { + dependencies.bagService.removeProduct(selectedProduct) products = dependencies.bagService.getBagContent() - dependencies.analytics.trackRemoveFromBag(productID: product.id) + dependencies.analytics.trackRemoveFromBag(productID: selectedProduct.product.id) } - func productCardViewModel(for product: SelectionProduct) -> HorizontalProductCardViewModel { + func productCardViewModel(for selectedProduct: SelectedProduct) -> HorizontalProductCardViewModel { .init( - image: product.media.first?.asImage?.url, - designer: product.brand.name, - name: product.name, + image: selectedProduct.media.first?.asImage?.url, + designer: selectedProduct.brand.name, + name: selectedProduct.name, colorTitle: L10n.Product.Color.title + ":", - color: product.colour?.name ?? "", + color: selectedProduct.colour?.name ?? "", sizeTitle: L10n.Product.Size.title + ":", - size: product.size == nil ? L10n.Product.OneSize.title : product.sizeText, - priceType: product.priceType + size: selectedProduct.size == nil ? L10n.Product.OneSize.title : selectedProduct.sizeText, + priceType: selectedProduct.priceType ) } } diff --git a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift index f8bbdc31..2428cfee 100644 --- a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift +++ b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift @@ -16,6 +16,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { private(set) var colorSelectionConfiguration: ColorAndSizingSelectorConfiguration = .init(items: []) private(set) var sizingSelectionConfiguration: ColorAndSizingSelectorConfiguration = .init(items: []) public let productId: String + private let initialSelectedProduct: SelectedProduct? private var product: Product? { guard case .success(let model) = state else { @@ -27,7 +28,7 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { private var selectedVariant: Product.Variant? { guard case .success(let model) = state else { - return baseProduct?.defaultVariant + return initialSelectedProduct?.selectedVariant ?? baseProduct?.defaultVariant } return model.selectedVariant @@ -88,16 +89,32 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { product?.priceType } - init(productId: String, product: Product?, dependencies: ProductDetailsDependencyContainer) { + init( + productId: String, + product: Product?, + selectedProduct: SelectedProduct? = nil, + dependencies: ProductDetailsDependencyContainer + ) { self.productId = productId + self.initialSelectedProduct = selectedProduct baseProduct = product self.dependencies = dependencies - if let baseProduct { + switch (product, selectedProduct) { + case (.some(let product), .none): buildColorAndSizingSelectionConfigurations( - product: baseProduct, - selectedVariant: baseProduct.defaultVariant + product: product, + selectedVariant: product.defaultVariant ) + + case (_, .some(let selectedProduct)): + buildColorAndSizingSelectionConfigurations( + product: selectedProduct.product, + selectedVariant: selectedProduct.selectedVariant + ) + + default: + break } } @@ -205,13 +222,14 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { return } - buildColorAndSizingSelectionConfigurations(product: product, selectedVariant: product.defaultVariant) - state = .success(.init(product: product, selectedVariant: product.defaultVariant)) + let selectedVariant = initialSelectedProduct?.selectedVariant ?? product.defaultVariant + buildColorAndSizingSelectionConfigurations(product: product, selectedVariant: selectedVariant) + state = .success(.init(product: product, selectedVariant: selectedVariant)) } - private func buildColorAndSizingSelectionConfigurations(product: Product, selectedVariant: Product.Variant?) { - buildColorSelectionConfiguration(product: product, selectedVariant: product.defaultVariant) - buildSizingSelectionConfiguration(product: product, selectedVariant: product.defaultVariant) + private func buildColorAndSizingSelectionConfigurations(product: Product, selectedVariant: Product.Variant) { + buildColorSelectionConfiguration(product: product, selectedVariant: selectedVariant) + buildSizingSelectionConfiguration(product: product, selectedVariant: selectedVariant) } private func buildColorSelectionConfiguration(product: Product, selectedVariant: Product.Variant?) { @@ -364,14 +382,14 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { state = .success(.init(product: product, selectedVariant: variant)) } - private var selectedProduct: SelectionProduct? { + private var selectedProduct: SelectedProduct? { guard let product, let selectedVariant else { - return nil + return initialSelectedProduct } - return SelectionProduct(product: product, selectedVariant: selectedVariant) + return SelectedProduct(product: product, selectedVariant: selectedVariant) } } diff --git a/Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift b/Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift index f5e9d687..7a7ebccb 100644 --- a/Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift +++ b/Alfie/Alfie/Views/ProductListing/ProductListingViewModel.swift @@ -14,7 +14,7 @@ final class ProductListingViewModel: ProductListingViewModelProtocol { @Published var style: ProductListingListStyle @Published var showRefine = false @Published var sortOption: String? - @Published private(set) var wishlistContent: [SelectionProduct] + @Published private(set) var wishlistContent: [SelectedProduct] @Published private(set) var state: PaginatedViewState private enum Constants { @@ -79,16 +79,17 @@ final class ProductListingViewModel: ProductListingViewModelProtocol { func didSelect(_: Product) {} func isFavoriteState(for product: Product) -> Bool { - wishlistContent.contains { $0.id == product.defaultVariant.sku } + wishlistContent.contains { $0.product.defaultVariant.sku == product.defaultVariant.sku } } func didTapAddToWishlist(for product: Product, isFavorite: Bool) { if !isFavorite { - let selectedProduct = SelectionProduct(product: product) + let selectedProduct = SelectedProduct(product: product) dependencies.wishlistService.addProduct(selectedProduct) - dependencies.analytics.trackAddToWishlist(productID: selectedProduct.id) + dependencies.analytics.trackAddToWishlist(productID: product.id) } else { - dependencies.wishlistService.removeProduct(product.defaultVariant.sku) + let selectedProduct = SelectedProduct(product: product) + dependencies.wishlistService.removeProduct(selectedProduct) dependencies.analytics.trackRemoveFromWishlist(productID: product.id) } wishlistContent = dependencies.wishlistService.getWishlistContent() diff --git a/Alfie/Alfie/Views/WishlistView/WishlistView.swift b/Alfie/Alfie/Views/WishlistView/WishlistView.swift index bae7008c..8948df21 100644 --- a/Alfie/Alfie/Views/WishlistView/WishlistView.swift +++ b/Alfie/Alfie/Views/WishlistView/WishlistView.swift @@ -6,6 +6,7 @@ import Mocks #endif struct WishlistView: View { + @EnvironmentObject var coordinador: Coordinator @StateObject private var viewModel: ViewModel init(viewModel: ViewModel) { @@ -22,11 +23,18 @@ struct WishlistView: View { spacing: Spacing.space200 ) { ForEach(viewModel.products) { product in - VerticalProductCard( - viewModel: viewModel.productCardViewModel(for: product) - ) { _, type in - handleUserAction(forProduct: product, actionType: type) - } + Button( + action: { coordinador.openDetails(for: product) }, + label: { + VerticalProductCard( + viewModel: viewModel.productCardViewModel(for: product) + ) { _, type in + handleUserAction(forProduct: product, actionType: type) + } + } + ) + .buttonStyle(.plain) + .listRowInsets(EdgeInsets()) } } .padding(.horizontal, Spacing.space200) @@ -41,7 +49,7 @@ struct WishlistView: View { // MARK: - Private Methods private extension WishlistView { - func handleUserAction(forProduct product: SelectionProduct, actionType: VerticalProductCard.ProductUserActionType) { + func handleUserAction(forProduct product: SelectedProduct, actionType: VerticalProductCard.ProductUserActionType) { // swiftlint:disable vertical_whitespace_between_cases switch actionType { case .remove: diff --git a/Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift b/Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift index fa6b3ca8..cbbbef72 100644 --- a/Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift +++ b/Alfie/Alfie/Views/WishlistView/WishlistViewModel.swift @@ -3,7 +3,7 @@ import Models import SharedUI final class WishlistViewModel: WishlistViewModelProtocol { - @Published private(set) var products: [SelectionProduct] + @Published private(set) var products: [SelectedProduct] private let dependencies: WishlistDependencyContainer @@ -18,32 +18,32 @@ final class WishlistViewModel: WishlistViewModelProtocol { products = dependencies.wishlistService.getWishlistContent() } - func didSelectDelete(for product: SelectionProduct) { - dependencies.wishlistService.removeProduct(product.id) - dependencies.analytics.trackRemoveFromWishlist(productID: product.id) + func didSelectDelete(for selectedProduct: SelectedProduct) { + dependencies.wishlistService.removeProduct(selectedProduct) + dependencies.analytics.trackRemoveFromWishlist(productID: selectedProduct.product.id) products = dependencies.wishlistService.getWishlistContent() } - func didTapAddToBag(for product: SelectionProduct) { - dependencies.bagService.addProduct(product) - dependencies.analytics.trackAddToBag(productID: product.id) + func didTapAddToBag(for selectedProduct: SelectedProduct) { + dependencies.bagService.addProduct(selectedProduct) + dependencies.analytics.trackAddToBag(productID: selectedProduct.product.id) } - func productCardViewModel(for product: SelectionProduct) -> VerticalProductCardViewModel { + func productCardViewModel(for selectedProduct: SelectedProduct) -> VerticalProductCardViewModel { .init( configuration: .init(size: .medium, hideDetails: false, actionType: .remove), - productId: product.id, - image: product.media.first?.asImage?.url, - designer: product.brand.name, - name: product.name, - priceType: product.priceType, + productId: selectedProduct.id, + image: selectedProduct.media.first?.asImage?.url, + designer: selectedProduct.brand.name, + name: selectedProduct.name, + priceType: selectedProduct.priceType, colorTitle: L10n.Product.Color.title + ":", - color: product.colour?.name ?? "", + color: selectedProduct.colour?.name ?? "", sizeTitle: L10n.Product.Size.title + ":", - size: product.size == nil ? L10n.Product.OneSize.title : product.sizeText, + size: selectedProduct.size == nil ? L10n.Product.OneSize.title : selectedProduct.sizeText, addToBagTitle: L10n.Product.AddToBag.Button.cta, outOfStockTitle: L10n.Product.OutOfStock.Button.cta, - isAddToBagDisabled: product.stock == .zero + isAddToBagDisabled: selectedProduct.stock == .zero ) } } diff --git a/Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift b/Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift index fb86eec2..22501648 100644 --- a/Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift +++ b/Alfie/AlfieKit/Sources/Core/Services/Bag/BagService.swift @@ -3,21 +3,21 @@ import Models // TODO: Update with an actual implementation with storage public final class BagService: BagServiceProtocol { - private var products: [SelectionProduct] = [] + private var products: [SelectedProduct] = [] public init() { } - public func addProduct(_ product: SelectionProduct) { + public func addProduct(_ product: SelectedProduct) { guard !products.contains(where: { $0.id == product.id }) else { return } products.append(product) } - public func removeProduct(_ product: SelectionProduct) { + public func removeProduct(_ product: SelectedProduct) { products = products.filter { $0.id != product.id } } - public func getBagContent() -> [SelectionProduct] { + public func getBagContent() -> [SelectedProduct] { products } } diff --git a/Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift b/Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift index 455bb51f..efa00e2e 100644 --- a/Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift +++ b/Alfie/AlfieKit/Sources/Core/Services/Wishlist/WishlistService.swift @@ -3,21 +3,21 @@ import Models // TODO: Update with an actual implementation with storage public final class WishlistService: WishlistServiceProtocol { - private var products: [SelectionProduct] = [] + private var products: [SelectedProduct] = [] public init() { } - public func addProduct(_ product: SelectionProduct) { + public func addProduct(_ product: SelectedProduct) { guard !products.contains(where: { $0.id == product.id }) else { return } products.append(product) } - public func removeProduct(_ productId: String) { - products = products.filter { $0.id != productId } + public func removeProduct(_ product: SelectedProduct) { + products = products.filter { $0.id != product.id } } - public func getWishlistContent() -> [SelectionProduct] { + public func getWishlistContent() -> [SelectedProduct] { products } } diff --git a/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockBagViewModel.swift b/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockBagViewModel.swift index d53bb98c..b3a95e52 100644 --- a/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockBagViewModel.swift +++ b/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockBagViewModel.swift @@ -1,9 +1,9 @@ import Models public class MockBagViewModel: BagViewModelProtocol { - public var products: [SelectionProduct] + public var products: [SelectedProduct] - public init(products: [SelectionProduct] = []) { + public init(products: [SelectedProduct] = []) { self.products = products } @@ -12,14 +12,14 @@ public class MockBagViewModel: BagViewModelProtocol { onViewDidAppearCalled?() } - public var onDidSelectDeleteCalled: ((SelectionProduct) -> Void)? - public func didSelectDelete(for product: SelectionProduct) { - onDidSelectDeleteCalled?(product) + public var onDidSelectDeleteCalled: ((SelectedProduct) -> Void)? + public func didSelectDelete(for selectedProduct: SelectedProduct) { + onDidSelectDeleteCalled?(selectedProduct) } - public var onProductCardViewModelCalled: ((SelectionProduct) -> HorizontalProductCardViewModel)? - public func productCardViewModel(for product: SelectionProduct) -> HorizontalProductCardViewModel { - onProductCardViewModelCalled?(product) ?? .init( + public var onProductCardViewModelCalled: ((SelectedProduct) -> HorizontalProductCardViewModel)? + public func productCardViewModel(for selectedProduct: SelectedProduct) -> HorizontalProductCardViewModel { + onProductCardViewModelCalled?(selectedProduct) ?? .init( image: nil, designer: "Yves Saint Laurent", name: "Rouge Pur Couture", diff --git a/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductListingViewModel.swift b/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductListingViewModel.swift index c24c1075..0e4de353 100644 --- a/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductListingViewModel.swift +++ b/Alfie/AlfieKit/Sources/Mocks/Core/Features/MockProductListingViewModel.swift @@ -6,7 +6,7 @@ import SwiftUI public class MockProductListingViewModel: ProductListingViewModelProtocol { public var state: PaginatedViewState public var products: [Product] - public var wishlistContent: [SelectionProduct] + public var wishlistContent: [SelectedProduct] public var title: String = "Title" public var totalNumberOfProducts: Int public var style: ProductListingListStyle = .grid @@ -17,7 +17,7 @@ public class MockProductListingViewModel: ProductListingViewModelProtocol { public init( state: PaginatedViewState, products: [Product] = [], - wishlistContent: [SelectionProduct] = [] + wishlistContent: [SelectedProduct] = [] ) { self.state = state self.products = products diff --git a/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockBagService.swift b/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockBagService.swift index a8289d36..e760f454 100644 --- a/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockBagService.swift +++ b/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockBagService.swift @@ -2,21 +2,21 @@ import Foundation import Models public final class MockBagService: BagServiceProtocol { - private var products: [SelectionProduct] = [] + private var products: [SelectedProduct] = [] public init() { } - public func addProduct(_ product: SelectionProduct) { + public func addProduct(_ product: SelectedProduct) { guard !products.contains(where: { $0.id == product.id }) else { return } products.append(product) } - public func removeProduct(_ product: SelectionProduct) { + public func removeProduct(_ product: SelectedProduct) { products = products.filter { $0.id != product.id } } - public func getBagContent() -> [SelectionProduct] { + public func getBagContent() -> [SelectedProduct] { products } } diff --git a/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift b/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift index 61fcf14a..dc255971 100644 --- a/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift +++ b/Alfie/AlfieKit/Sources/Mocks/Core/Services/MockWishlistService.swift @@ -2,21 +2,21 @@ import Foundation import Models public final class MockWishlistService: WishlistServiceProtocol { - private var products: [SelectionProduct] = [] + private var products: [SelectedProduct] = [] public init() { } - public func addProduct(_ product: SelectionProduct) { + public func addProduct(_ product: SelectedProduct) { guard !products.contains(where: { $0.id == product.id }) else { return } products.append(product) } - public func removeProduct(_ productId: String) { - products = products.filter { $0.id != productId } + public func removeProduct(_ product: SelectedProduct) { + products = products.filter { $0.id != product.id } } - public func getWishlistContent() -> [SelectionProduct] { + public func getWishlistContent() -> [SelectedProduct] { products } } diff --git a/Alfie/AlfieKit/Sources/Models/Features/BagViewModelProtocol.swift b/Alfie/AlfieKit/Sources/Models/Features/BagViewModelProtocol.swift index 9b16448b..41fdb4a1 100644 --- a/Alfie/AlfieKit/Sources/Models/Features/BagViewModelProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Features/BagViewModelProtocol.swift @@ -1,9 +1,9 @@ import Foundation public protocol BagViewModelProtocol: ObservableObject { - var products: [SelectionProduct] { get } + var products: [SelectedProduct] { get } func viewDidAppear() - func didSelectDelete(for product: SelectionProduct) - func productCardViewModel(for product: SelectionProduct) -> HorizontalProductCardViewModel + func didSelectDelete(for selectedProduct: SelectedProduct) + func productCardViewModel(for selectedProduct: SelectedProduct) -> HorizontalProductCardViewModel } diff --git a/Alfie/AlfieKit/Sources/Models/Features/ProductListingViewModelProtocol.swift b/Alfie/AlfieKit/Sources/Models/Features/ProductListingViewModelProtocol.swift index ae497a09..2d03eef3 100644 --- a/Alfie/AlfieKit/Sources/Models/Features/ProductListingViewModelProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Features/ProductListingViewModelProtocol.swift @@ -26,7 +26,7 @@ public enum ProductListingViewMode { public protocol ProductListingViewModelProtocol: ObservableObject { var state: PaginatedViewState { get } var products: [Product] { get } - var wishlistContent: [SelectionProduct] { get } + var wishlistContent: [SelectedProduct] { get } var style: ProductListingListStyle { get set } var showRefine: Bool { get set } var sortOption: String? { get set } diff --git a/Alfie/AlfieKit/Sources/Models/Features/WishlistViewModelProtocol.swift b/Alfie/AlfieKit/Sources/Models/Features/WishlistViewModelProtocol.swift index 61be342b..c69cabcf 100644 --- a/Alfie/AlfieKit/Sources/Models/Features/WishlistViewModelProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Features/WishlistViewModelProtocol.swift @@ -1,10 +1,10 @@ import Foundation public protocol WishlistViewModelProtocol: ObservableObject { - var products: [SelectionProduct] { get } + var products: [SelectedProduct] { get } func viewDidAppear() - func didSelectDelete(for product: SelectionProduct) - func didTapAddToBag(for product: SelectionProduct) - func productCardViewModel(for product: SelectionProduct) -> VerticalProductCardViewModel + func didSelectDelete(for selectedProduct: SelectedProduct) + func didTapAddToBag(for selectedProduct: SelectedProduct) + func productCardViewModel(for selectedProduct: SelectedProduct) -> VerticalProductCardViewModel } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift new file mode 100644 index 00000000..a5f95b7f --- /dev/null +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift @@ -0,0 +1,53 @@ +import Foundation + +public struct SelectedProduct { + public let product: Product + public let selectedVariant: Product.Variant + + public var name: String { + product.name + } + + public var brand: Brand { + product.brand + } + + public var size: Product.ProductSize? { + selectedVariant.size + } + + public var colour: Product.Colour? { + selectedVariant.colour + } + + public var stock: Int { + selectedVariant.stock + } + + public var price: Price { + selectedVariant.price + } + + public var media: [Media] { + colour?.media ?? [] + } + + public init(product: Product, selectedVariant: Product.Variant? = nil) { + self.product = product + self.selectedVariant = selectedVariant ?? product.defaultVariant + } +} + +extension SelectedProduct: Identifiable, Equatable, Hashable { + public var id: String { + "\(product.id)-\(selectedVariant.sku)" + } + + public static func == (lhs: SelectedProduct, rhs: SelectedProduct) -> Bool { + lhs.id == rhs.id + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectionProduct.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/SelectionProduct.swift deleted file mode 100644 index bb425f72..00000000 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectionProduct.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -public struct SelectionProduct: Identifiable { - public let id: String - public let name: String - public let brand: Brand - public let size: Product.ProductSize? - public let colour: Product.Colour? - public let stock: Int - public let price: Price - - public var media: [Media] { - colour?.media ?? [] - } - - public init(product: Product, selectedVariant: Product.Variant? = nil) { - let selectedVariant = selectedVariant ?? product.defaultVariant - - self.id = selectedVariant.sku - self.name = product.name - self.brand = product.brand - self.size = selectedVariant.size - self.colour = selectedVariant.colour - self.stock = selectedVariant.stock - self.price = selectedVariant.price - } -} diff --git a/Alfie/AlfieKit/Sources/Models/Services/Bag/BagServiceProtocol.swift b/Alfie/AlfieKit/Sources/Models/Services/Bag/BagServiceProtocol.swift index 4ec49299..05912e09 100644 --- a/Alfie/AlfieKit/Sources/Models/Services/Bag/BagServiceProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Services/Bag/BagServiceProtocol.swift @@ -1,7 +1,7 @@ import Foundation public protocol BagServiceProtocol { - func addProduct(_ product: SelectionProduct) - func removeProduct(_ product: SelectionProduct) - func getBagContent() -> [SelectionProduct] + func addProduct(_ product: SelectedProduct) + func removeProduct(_ product: SelectedProduct) + func getBagContent() -> [SelectedProduct] } diff --git a/Alfie/AlfieKit/Sources/Models/Services/Wishlist/WishlistServiceProtocol.swift b/Alfie/AlfieKit/Sources/Models/Services/Wishlist/WishlistServiceProtocol.swift index 57305abb..6b8bc58d 100644 --- a/Alfie/AlfieKit/Sources/Models/Services/Wishlist/WishlistServiceProtocol.swift +++ b/Alfie/AlfieKit/Sources/Models/Services/Wishlist/WishlistServiceProtocol.swift @@ -1,7 +1,7 @@ import Foundation public protocol WishlistServiceProtocol { - func addProduct(_ product: SelectionProduct) - func removeProduct(_ productId: String) - func getWishlistContent() -> [SelectionProduct] + func addProduct(_ product: SelectedProduct) + func removeProduct(_ product: SelectedProduct) + func getWishlistContent() -> [SelectedProduct] } From 2f4ec6f6a340aaa943a0c2f2ffebc78b5a35354b Mon Sep 17 00:00:00 2001 From: JoaoPinhoMinder Date: Mon, 31 Mar 2025 14:17:55 +0100 Subject: [PATCH 2/6] ALFMOB-149: Code review initialize modifications --- Alfie/Alfie/Navigation/ViewFactory.swift | 59 ++++--------------- .../ProductDetailsViewModel.swift | 29 +++++---- 2 files changed, 28 insertions(+), 60 deletions(-) diff --git a/Alfie/Alfie/Navigation/ViewFactory.swift b/Alfie/Alfie/Navigation/ViewFactory.swift index 49959ff2..24628296 100644 --- a/Alfie/Alfie/Navigation/ViewFactory.swift +++ b/Alfie/Alfie/Navigation/ViewFactory.swift @@ -136,56 +136,19 @@ final class ViewFactory: ViewFactoryProtocol { .withToolbar(for: .wishlist) case .productDetails(let type): - switch type { - case .id(let id): - ProductDetailsView( - viewModel: ProductDetailsViewModel( - productId: id, - product: nil, - dependencies: ProductDetailsDependencyContainer( - productService: serviceProvider.productService, - webUrlProvider: serviceProvider.webUrlProvider, - bagService: serviceProvider.bagService, - wishlistService: serviceProvider.wishlistService, - configurationService: serviceProvider.configurationService, - analytics: serviceProvider.analytics - ) - ) - ) - - case .product(let product): - ProductDetailsView( - viewModel: ProductDetailsViewModel( - productId: product.id, - product: product, - dependencies: ProductDetailsDependencyContainer( - productService: serviceProvider.productService, - webUrlProvider: serviceProvider.webUrlProvider, - bagService: serviceProvider.bagService, - wishlistService: serviceProvider.wishlistService, - configurationService: serviceProvider.configurationService, - analytics: serviceProvider.analytics - ) - ) - ) - - case .selectedProduct(let selectedProduct): - ProductDetailsView( - viewModel: ProductDetailsViewModel( - productId: selectedProduct.product.id, - product: selectedProduct.product, - selectedProduct: selectedProduct, - dependencies: ProductDetailsDependencyContainer( - productService: serviceProvider.productService, - webUrlProvider: serviceProvider.webUrlProvider, - bagService: serviceProvider.bagService, - wishlistService: serviceProvider.wishlistService, - configurationService: serviceProvider.configurationService, - analytics: serviceProvider.analytics - ) + ProductDetailsView( + viewModel: ProductDetailsViewModel( + productKind: type, + dependencies: ProductDetailsDependencyContainer( + productService: serviceProvider.productService, + webUrlProvider: serviceProvider.webUrlProvider, + bagService: serviceProvider.bagService, + wishlistService: serviceProvider.wishlistService, + configurationService: serviceProvider.configurationService, + analytics: serviceProvider.analytics ) ) - } + ) case .recentSearches: let viewModel = RecentSearchesViewModel(recentsService: serviceProvider.recentsService) diff --git a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift index 2428cfee..36b38800 100644 --- a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift +++ b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift @@ -90,31 +90,36 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { } init( - productId: String, - product: Product?, - selectedProduct: SelectedProduct? = nil, + productKind: ThemedProductDetailsScreen, dependencies: ProductDetailsDependencyContainer ) { - self.productId = productId - self.initialSelectedProduct = selectedProduct - baseProduct = product self.dependencies = dependencies - switch (product, selectedProduct) { - case (.some(let product), .none): + switch productKind { + case .id(let productId): + self.productId = productId + self.initialSelectedProduct = nil + self.baseProduct = nil + + case .product(let product): + self.productId = product.id + self.initialSelectedProduct = nil + self.baseProduct = product + buildColorAndSizingSelectionConfigurations( product: product, selectedVariant: product.defaultVariant ) - case (_, .some(let selectedProduct)): + case .selectedProduct(let selectedProduct): + self.productId = selectedProduct.product.id + self.initialSelectedProduct = selectedProduct + baseProduct = selectedProduct.product + buildColorAndSizingSelectionConfigurations( product: selectedProduct.product, selectedVariant: selectedProduct.selectedVariant ) - - default: - break } } From ab0229bd8c84f0d6f86ab4274bf4e67b0c484625 Mon Sep 17 00:00:00 2001 From: JoaoPinhoMinder Date: Mon, 31 Mar 2025 15:09:09 +0100 Subject: [PATCH 3/6] ALFMOB-149: Code review Equatable & Hashable --- .../Sources/Models/Models/Base/Media.swift | 2 +- .../Models/Models/Base/MediaImage.swift | 14 +++- .../Models/Models/Base/MediaVideo.swift | 30 +++++++- .../Sources/Models/Models/Base/Price.swift | 26 ++++++- .../Models/Models/Base/SelectedProduct.swift | 6 +- .../Models/Models/Product/Product.swift | 70 +++++++++++++++++-- 6 files changed, 136 insertions(+), 12 deletions(-) diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/Media.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/Media.swift index ea7fe468..0a458780 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/Media.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/Media.swift @@ -1,6 +1,6 @@ import Foundation -public enum Media { +public enum Media: Equatable, Hashable { case image(MediaImage) case video(MediaVideo) diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/MediaImage.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/MediaImage.swift index bdebde91..34d1e8b5 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/MediaImage.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/MediaImage.swift @@ -1,6 +1,6 @@ import Foundation -public struct MediaImage { +public struct MediaImage: Equatable, Hashable { /// A description of the contents of the image for accessibility purposes. public let alt: String? /// The media content type. @@ -13,4 +13,16 @@ public struct MediaImage { self.mediaContentType = mediaContentType self.url = url } + + public static func == (lhs: MediaImage, rhs: MediaImage) -> Bool { + lhs.alt == rhs.alt + && lhs.mediaContentType == rhs.mediaContentType + && lhs.url == rhs.url + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(alt) + hasher.combine(mediaContentType) + hasher.combine(url) + } } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/MediaVideo.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/MediaVideo.swift index fcbd3981..159845d9 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/MediaVideo.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/MediaVideo.swift @@ -1,6 +1,6 @@ import Foundation -public struct MediaVideo { +public struct MediaVideo: Equatable, Hashable { /// A description of the contents of the video for accessibility purposes. public let alt: String? /// The media content type. @@ -16,9 +16,23 @@ public struct MediaVideo { self.previewImage = previewImage self.sources = sources } + + public static func == (lhs: MediaVideo, rhs: MediaVideo) -> Bool { + lhs.alt == rhs.alt + && lhs.mediaContentType == rhs.mediaContentType + && lhs.previewImage == rhs.previewImage + && lhs.sources == rhs.sources + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(alt) + hasher.combine(mediaContentType) + hasher.combine(previewImage) + hasher.combine(sources) + } } -public struct VideoSource { +public struct VideoSource: Equatable, Hashable { public enum VideoFormat: String { case mp4 = "MP4" case webm = "WEBM" @@ -37,4 +51,16 @@ public struct VideoSource { self.mimeType = mimeType self.url = url } + + public static func == (lhs: VideoSource, rhs: VideoSource) -> Bool { + lhs.format == rhs.format + && lhs.mimeType == rhs.mimeType + && lhs.url == rhs.url + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(format) + hasher.combine(mimeType) + hasher.combine(url) + } } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/Price.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/Price.swift index 2fbde665..01a9740d 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/Price.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/Price.swift @@ -1,6 +1,6 @@ import Foundation -public struct Price { +public struct Price: Equatable, Hashable { /// The current price. public let amount: Money /// If discounted, the previous price. @@ -10,6 +10,16 @@ public struct Price { self.amount = amount self.was = was } + + public static func == (lhs: Price, rhs: Price) -> Bool { + lhs.amount == rhs.amount + && lhs.was == rhs.was + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(amount) + hasher.combine(was) + } } public struct PriceRange { @@ -24,7 +34,7 @@ public struct PriceRange { } } -public struct Money { +public struct Money: Equatable, Hashable { /// The 3-letter currency code e.g. AUD. public let currencyCode: String /// The amount in minor units (e.g. for $1.23 this will be 123). @@ -37,4 +47,16 @@ public struct Money { self.amount = amount self.amountFormatted = amountFormatted } + + public static func == (lhs: Money, rhs: Money) -> Bool { + lhs.currencyCode == rhs.currencyCode + && lhs.amount == rhs.amount + && lhs.amountFormatted == rhs.amountFormatted + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(currencyCode) + hasher.combine(amount) + hasher.combine(amountFormatted) + } } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift index a5f95b7f..7d38afd8 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift @@ -44,10 +44,12 @@ extension SelectedProduct: Identifiable, Equatable, Hashable { } public static func == (lhs: SelectedProduct, rhs: SelectedProduct) -> Bool { - lhs.id == rhs.id + lhs.product == rhs.product + && lhs.selectedVariant == rhs.selectedVariant } public func hash(into hasher: inout Hasher) { - hasher.combine(id) + hasher.combine(product) + hasher.combine(selectedVariant) } } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift b/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift index 3a25850a..3ba5a674 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift @@ -67,7 +67,7 @@ public struct Product: Identifiable, Equatable, Hashable { // MARK: - Product Properties Type extension Product { - public struct Variant { + public struct Variant: Equatable, Hashable { /// A unique identifier for the variant. public let sku: String /// Size, if applicable. @@ -101,9 +101,27 @@ extension Product { self.stock = stock self.price = price } + + public static func == (lhs: Product.Variant, rhs: Product.Variant) -> Bool { + lhs.sku == rhs.sku + && lhs.size == rhs.size + && lhs.colour == rhs.colour + && lhs.attributes == rhs.attributes + && lhs.stock == rhs.stock + && lhs.price == rhs.price + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(sku) + hasher.combine(size) + hasher.combine(colour) + hasher.combine(attributes) + hasher.combine(stock) + hasher.combine(price) + } } - public struct Colour { + public struct Colour: Equatable, Hashable { /// Unique ID for the colour. public let id: String /// Image resolver for the colour swatch. @@ -119,9 +137,23 @@ extension Product { self.name = name self.media = media } + + public static func == (lhs: Colour, rhs: Colour) -> Bool { + lhs.id == rhs.id + && lhs.swatch == rhs.swatch + && lhs.name == rhs.name + && lhs.media == rhs.media + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(swatch) + hasher.combine(name) + hasher.combine(media) + } } - public struct ProductSize { + public struct ProductSize: Equatable, Hashable { /// Unique size ID. public let id: String /// The size value (e.g. XS). @@ -146,9 +178,25 @@ extension Product { self.description = description self.sizeGuide = sizeGuide } + + public static func == (lhs: ProductSize, rhs: ProductSize) -> Bool { + lhs.id == rhs.id + && lhs.value == rhs.value + && lhs.scale == rhs.scale + && lhs.description == rhs.description + && lhs.sizeGuide == rhs.sizeGuide + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(value) + hasher.combine(scale) + hasher.combine(description) + hasher.combine(sizeGuide) + } } - public struct SizeGuide { + public struct SizeGuide: Equatable, Hashable { /// Unique size guide ID. public let id: String /// The name of the size guide (e.g. Men's shoes size guide). @@ -164,5 +212,19 @@ extension Product { self.description = description self.sizes = sizes } + + public static func == (lhs: SizeGuide, rhs: SizeGuide) -> Bool { + lhs.id == rhs.id + && lhs.name == rhs.name + && lhs.description == rhs.description + && lhs.sizes == rhs.sizes + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(name) + hasher.combine(description) + hasher.combine(sizes) + } } } From 4e14a5eda55e90038f7f09df15e859bce39d7b3b Mon Sep 17 00:00:00 2001 From: JoaoPinhoMinder Date: Mon, 31 Mar 2025 15:33:08 +0100 Subject: [PATCH 4/6] LFMOB-162: Fix unit tests --- .../Features/ProductDetailsViewModelTests.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift b/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift index 0a52c66e..cefad877 100644 --- a/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift +++ b/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift @@ -816,8 +816,13 @@ final class ProductDetailsViewModelTests: XCTestCase { // MARK: - Helper methods private func initViewModel(productId: String = "", product: Product? = nil) { - sut = .init(productId: productId, - product: product, - dependencies: mockDependencies) + let productKind: ThemedProductDetailsScreen + if let product = product { + productKind = .product(product) + } else { + productKind = .id(productId) + } + + sut = .init(productKind: productKind, dependencies: mockDependencies) } } From 7059e191800e58d6f923b526475199bebaaece35 Mon Sep 17 00:00:00 2001 From: JoaoPinhoMinder Date: Tue, 1 Apr 2025 12:24:21 +0100 Subject: [PATCH 5/6] ALFMOB-162: Code review --- .../xcshareddata/swiftpm/Package.resolved | 8 +- Alfie/Alfie/Navigation/Coordinator.swift | 6 +- .../DeepLinking/DeepLinkHandler.swift | 2 +- Alfie/Alfie/Navigation/Screen.swift | 8 +- Alfie/Alfie/Navigation/ViewFactory.swift | 4 +- .../ProductDetails/ProductDetailsView.swift | 2 +- .../ProductDetailsViewModel.swift | 4 +- .../Sources/Models/Models/Base/Brand.swift | 2 +- .../Sources/Models/Models/Base/Media.swift | 2 +- .../Models/Models/Base/MediaImage.swift | 14 +--- .../Models/Models/Base/MediaVideo.swift | 30 +------ .../Sources/Models/Models/Base/Price.swift | 28 +------ .../Models/Models/Base/SelectedProduct.swift | 12 +-- .../Models/Models/Product/Product.swift | 80 ++----------------- .../ProductDetailsViewModelTests.swift | 46 +++++------ .../Handlers/DeepLinkHandlerTests.swift | 2 +- 16 files changed, 53 insertions(+), 197 deletions(-) diff --git a/Alfie/Alfie.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Alfie/Alfie.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b1077590..34b547f5 100644 --- a/Alfie/Alfie.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Alfie/Alfie.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb", - "version" : "8.0.2" + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" } }, { @@ -230,8 +230,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", - "version" : "510.0.3" + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" } }, { diff --git a/Alfie/Alfie/Navigation/Coordinator.swift b/Alfie/Alfie/Navigation/Coordinator.swift index a7199899..97f127a2 100644 --- a/Alfie/Alfie/Navigation/Coordinator.swift +++ b/Alfie/Alfie/Navigation/Coordinator.swift @@ -135,15 +135,15 @@ final class Coordinator: ObservableObject, CoordinatorProtocol { // MARK: - Product Details public func openDetails(for productId: String) { - navigationAdapter.push(.productDetails(.id(productId))) + navigationAdapter.push(.productDetails(configuration: .id(productId))) } public func openDetails(for product: Product) { - navigationAdapter.push(.productDetails(.product(product))) + navigationAdapter.push(.productDetails(configuration: .product(product))) } public func openDetails(for selectedProduct: SelectedProduct) { - navigationAdapter.push(.productDetails(.selectedProduct(selectedProduct))) + navigationAdapter.push(.productDetails(configuration: .selectedProduct(selectedProduct))) } // MARK: - Brands diff --git a/Alfie/Alfie/Navigation/DeepLinking/DeepLinkHandler.swift b/Alfie/Alfie/Navigation/DeepLinking/DeepLinkHandler.swift index b80195d3..6e2feaea 100644 --- a/Alfie/Alfie/Navigation/DeepLinking/DeepLinkHandler.swift +++ b/Alfie/Alfie/Navigation/DeepLinking/DeepLinkHandler.swift @@ -92,7 +92,7 @@ final class DeepLinkHandler: DeepLinkHandlerProtocol { case .productDetail(let productId, _, _, _): // TODO: currently the API does not support fetching a product by the StyleNumber (that is parsed from the URL), just by ProductID, so all requests will return "not found" - target = Screen.productDetails(.id(productId)) + target = Screen.productDetails(configuration: .id(productId)) case .webView(let url): target = Screen.webView(url: url, title: "") diff --git a/Alfie/Alfie/Navigation/Screen.swift b/Alfie/Alfie/Navigation/Screen.swift index c5e19a4f..d98804b6 100644 --- a/Alfie/Alfie/Navigation/Screen.swift +++ b/Alfie/Alfie/Navigation/Screen.swift @@ -13,7 +13,7 @@ enum Screen: ScreenProtocol { case recentSearches case forceAppUpdate case debugMenu - case productDetails(_ type: ThemedProductDetailsScreen) + case productDetails(configuration: ProductDetailsConfiguration) case productListing(configuration: ProductListingScreenConfiguration) case categoryList(_ categories: [NavigationItem], title: String) @@ -35,18 +35,18 @@ enum TabScreen: ScreenProtocol { static let allCases: [TabScreen] = [.home(), .shop(), .wishlist, .bag] } -enum HomeTabConfig: Equatable, Hashable { +enum HomeTabConfig: Hashable { case loggedIn(username: String, memberSince: Int) case loggedOut } -enum ThemedProductDetailsScreen: Equatable, Hashable { +enum ProductDetailsConfiguration: Hashable { case id(_ id: String) case product(_ product: Product) case selectedProduct(_ selectedProduct: SelectedProduct) } -struct ProductListingScreenConfiguration: Equatable, Hashable { +struct ProductListingScreenConfiguration: Hashable { let category: String? let searchText: String? let urlQueryParameters: [String: String]? diff --git a/Alfie/Alfie/Navigation/ViewFactory.swift b/Alfie/Alfie/Navigation/ViewFactory.swift index 24628296..ab10523d 100644 --- a/Alfie/Alfie/Navigation/ViewFactory.swift +++ b/Alfie/Alfie/Navigation/ViewFactory.swift @@ -135,10 +135,10 @@ final class ViewFactory: ViewFactoryProtocol { wishlistView .withToolbar(for: .wishlist) - case .productDetails(let type): + case .productDetails(let configuration): ProductDetailsView( viewModel: ProductDetailsViewModel( - productKind: type, + productDetailsConfiguration: configuration, dependencies: ProductDetailsDependencyContainer( productService: serviceProvider.productService, webUrlProvider: serviceProvider.webUrlProvider, diff --git a/Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift b/Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift index 15150a52..fd6e76a5 100644 --- a/Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift +++ b/Alfie/Alfie/Views/ProductDetails/ProductDetailsView.swift @@ -74,7 +74,7 @@ struct ProductDetailsView: View { .writingSize(to: $viewSize) } } - .withToolbar(for: .productDetails(.id(viewModel.productId))) + .withToolbar(for: .productDetails(configuration: .id(viewModel.productId))) .toolbar { if !viewModel.state.didFail { ToolbarItem(placement: .principal) { diff --git a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift index 36b38800..7b7db47d 100644 --- a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift +++ b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift @@ -90,12 +90,12 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { } init( - productKind: ThemedProductDetailsScreen, + productDetailsConfiguration: ProductDetailsConfiguration, dependencies: ProductDetailsDependencyContainer ) { self.dependencies = dependencies - switch productKind { + switch productDetailsConfiguration { case .id(let productId): self.productId = productId self.initialSelectedProduct = nil diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/Brand.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/Brand.swift index 428ecba2..4fd1e35b 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/Brand.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/Brand.swift @@ -1,6 +1,6 @@ import Foundation -public struct Brand: Identifiable, Equatable { +public struct Brand: Identifiable, Hashable { /// The ID for the brand public let id: String /// The display name of the brand diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/Media.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/Media.swift index 0a458780..8dea2824 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/Media.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/Media.swift @@ -1,6 +1,6 @@ import Foundation -public enum Media: Equatable, Hashable { +public enum Media: Hashable { case image(MediaImage) case video(MediaVideo) diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/MediaImage.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/MediaImage.swift index 34d1e8b5..2424622f 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/MediaImage.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/MediaImage.swift @@ -1,6 +1,6 @@ import Foundation -public struct MediaImage: Equatable, Hashable { +public struct MediaImage: Hashable { /// A description of the contents of the image for accessibility purposes. public let alt: String? /// The media content type. @@ -13,16 +13,4 @@ public struct MediaImage: Equatable, Hashable { self.mediaContentType = mediaContentType self.url = url } - - public static func == (lhs: MediaImage, rhs: MediaImage) -> Bool { - lhs.alt == rhs.alt - && lhs.mediaContentType == rhs.mediaContentType - && lhs.url == rhs.url - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(alt) - hasher.combine(mediaContentType) - hasher.combine(url) - } } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/MediaVideo.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/MediaVideo.swift index 159845d9..b8867753 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/MediaVideo.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/MediaVideo.swift @@ -1,6 +1,6 @@ import Foundation -public struct MediaVideo: Equatable, Hashable { +public struct MediaVideo: Hashable { /// A description of the contents of the video for accessibility purposes. public let alt: String? /// The media content type. @@ -16,23 +16,9 @@ public struct MediaVideo: Equatable, Hashable { self.previewImage = previewImage self.sources = sources } - - public static func == (lhs: MediaVideo, rhs: MediaVideo) -> Bool { - lhs.alt == rhs.alt - && lhs.mediaContentType == rhs.mediaContentType - && lhs.previewImage == rhs.previewImage - && lhs.sources == rhs.sources - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(alt) - hasher.combine(mediaContentType) - hasher.combine(previewImage) - hasher.combine(sources) - } } -public struct VideoSource: Equatable, Hashable { +public struct VideoSource: Hashable { public enum VideoFormat: String { case mp4 = "MP4" case webm = "WEBM" @@ -51,16 +37,4 @@ public struct VideoSource: Equatable, Hashable { self.mimeType = mimeType self.url = url } - - public static func == (lhs: VideoSource, rhs: VideoSource) -> Bool { - lhs.format == rhs.format - && lhs.mimeType == rhs.mimeType - && lhs.url == rhs.url - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(format) - hasher.combine(mimeType) - hasher.combine(url) - } } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/Price.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/Price.swift index 01a9740d..70c1b4d1 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/Price.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/Price.swift @@ -1,6 +1,6 @@ import Foundation -public struct Price: Equatable, Hashable { +public struct Price: Hashable { /// The current price. public let amount: Money /// If discounted, the previous price. @@ -10,19 +10,9 @@ public struct Price: Equatable, Hashable { self.amount = amount self.was = was } - - public static func == (lhs: Price, rhs: Price) -> Bool { - lhs.amount == rhs.amount - && lhs.was == rhs.was - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(amount) - hasher.combine(was) - } } -public struct PriceRange { +public struct PriceRange: Hashable { /// The lowest price. public let low: Money /// The highest price if not a 'from' range. @@ -34,7 +24,7 @@ public struct PriceRange { } } -public struct Money: Equatable, Hashable { +public struct Money: Hashable { /// The 3-letter currency code e.g. AUD. public let currencyCode: String /// The amount in minor units (e.g. for $1.23 this will be 123). @@ -47,16 +37,4 @@ public struct Money: Equatable, Hashable { self.amount = amount self.amountFormatted = amountFormatted } - - public static func == (lhs: Money, rhs: Money) -> Bool { - lhs.currencyCode == rhs.currencyCode - && lhs.amount == rhs.amount - && lhs.amountFormatted == rhs.amountFormatted - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(currencyCode) - hasher.combine(amount) - hasher.combine(amountFormatted) - } } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift b/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift index 7d38afd8..a8ece74d 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Base/SelectedProduct.swift @@ -38,18 +38,8 @@ public struct SelectedProduct { } } -extension SelectedProduct: Identifiable, Equatable, Hashable { +extension SelectedProduct: Identifiable, Hashable { public var id: String { "\(product.id)-\(selectedVariant.sku)" } - - public static func == (lhs: SelectedProduct, rhs: SelectedProduct) -> Bool { - lhs.product == rhs.product - && lhs.selectedVariant == rhs.selectedVariant - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(product) - hasher.combine(selectedVariant) - } } diff --git a/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift b/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift index 3ba5a674..99e7905f 100644 --- a/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift +++ b/Alfie/AlfieKit/Sources/Models/Models/Product/Product.swift @@ -1,6 +1,6 @@ import Foundation -public struct Product: Identifiable, Equatable, Hashable { +public struct Product: Identifiable, Hashable { /// Unique ID for the product and its variants. public let id: String /// App refers to products (including variants) as style numbers, so this is the product's unique identifier. @@ -54,20 +54,12 @@ public struct Product: Identifiable, Equatable, Hashable { self.variants = variants self.colours = colours } - - public static func == (lhs: Product, rhs: Product) -> Bool { - lhs.styleNumber == rhs.styleNumber - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(styleNumber) - } } // MARK: - Product Properties Type extension Product { - public struct Variant: Equatable, Hashable { + public struct Variant: Hashable { /// A unique identifier for the variant. public let sku: String /// Size, if applicable. @@ -101,27 +93,9 @@ extension Product { self.stock = stock self.price = price } - - public static func == (lhs: Product.Variant, rhs: Product.Variant) -> Bool { - lhs.sku == rhs.sku - && lhs.size == rhs.size - && lhs.colour == rhs.colour - && lhs.attributes == rhs.attributes - && lhs.stock == rhs.stock - && lhs.price == rhs.price - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(sku) - hasher.combine(size) - hasher.combine(colour) - hasher.combine(attributes) - hasher.combine(stock) - hasher.combine(price) - } } - public struct Colour: Equatable, Hashable { + public struct Colour: Hashable { /// Unique ID for the colour. public let id: String /// Image resolver for the colour swatch. @@ -137,23 +111,9 @@ extension Product { self.name = name self.media = media } - - public static func == (lhs: Colour, rhs: Colour) -> Bool { - lhs.id == rhs.id - && lhs.swatch == rhs.swatch - && lhs.name == rhs.name - && lhs.media == rhs.media - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(swatch) - hasher.combine(name) - hasher.combine(media) - } } - public struct ProductSize: Equatable, Hashable { + public struct ProductSize: Hashable { /// Unique size ID. public let id: String /// The size value (e.g. XS). @@ -178,25 +138,9 @@ extension Product { self.description = description self.sizeGuide = sizeGuide } - - public static func == (lhs: ProductSize, rhs: ProductSize) -> Bool { - lhs.id == rhs.id - && lhs.value == rhs.value - && lhs.scale == rhs.scale - && lhs.description == rhs.description - && lhs.sizeGuide == rhs.sizeGuide - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(value) - hasher.combine(scale) - hasher.combine(description) - hasher.combine(sizeGuide) - } } - public struct SizeGuide: Equatable, Hashable { + public struct SizeGuide: Hashable { /// Unique size guide ID. public let id: String /// The name of the size guide (e.g. Men's shoes size guide). @@ -212,19 +156,5 @@ extension Product { self.description = description self.sizes = sizes } - - public static func == (lhs: SizeGuide, rhs: SizeGuide) -> Bool { - lhs.id == rhs.id - && lhs.name == rhs.name - && lhs.description == rhs.description - && lhs.sizes == rhs.sizes - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(name) - hasher.combine(description) - hasher.combine(sizes) - } } } diff --git a/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift b/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift index cefad877..3c59f1eb 100644 --- a/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift +++ b/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift @@ -53,7 +53,7 @@ final class ProductDetailsViewModelTests: XCTestCase { brand: .fixture(name: "Product Brand"), defaultVariant: variant, variants: [variant]) - initViewModel(product: product) + initViewModel(productDetailsConfiguration: .product(product)) XCTAssertEqual(sut.productName, product.name) XCTAssertEqual(sut.productTitle, product.brand.name) let colorSelectionConfiguration = sut.colorSelectionConfiguration @@ -73,7 +73,7 @@ final class ProductDetailsViewModelTests: XCTestCase { let product = Product.fixture(name: "Product Name", brand: .fixture(name: "Product Brand"), variants: [variant]) - initViewModel(product: product) + initViewModel(productDetailsConfiguration: .product(product)) XCTAssertEqual(sut.sizingSelectionConfiguration.items.count, 0) } @@ -84,7 +84,7 @@ final class ProductDetailsViewModelTests: XCTestCase { XCTAssertTrue(sut.productId.isEmpty) let productId = "1" - initViewModel(productId: productId) + initViewModel(productDetailsConfiguration: .id(productId)) XCTAssertEqual(sut.productId, productId) } @@ -95,7 +95,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_title_is_available_after_fetching_product() { let product = Product.fixture(brand: .fixture(name: "Product Brand")) - initViewModel(product: product) + initViewModel(productDetailsConfiguration: .product(product)) XCTAssertEmitsValue(from: sut.$state.drop(while: \.isLoading), afterTrigger: { self.sut.viewDidAppear() }) @@ -109,7 +109,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_name_is_available_after_fetching_product() { let product = Product.fixture(name: "Product Name") - initViewModel(product: product) + initViewModel(productDetailsConfiguration: .product(product)) XCTAssertEmitsValue(from: sut.$state.drop(while: \.isLoading), afterTrigger: { self.sut.viewDidAppear() }) @@ -127,8 +127,10 @@ final class ProductDetailsViewModelTests: XCTestCase { .image(.fixture(url: URL(string: "http://some.media.url.2")!)), ]) let variant = Product.Variant.fixture(colour: color) - let product = Product.fixture(name: "Product Name", defaultVariant: variant, variants: [variant]) - initViewModel(product: product) + let product = Product.fixture(name: "Product Name", + defaultVariant: variant, + variants: [variant]) + initViewModel(productDetailsConfiguration: .product(product)) XCTAssertEmitsValue(from: sut.$state.drop(while: \.isLoading), afterTrigger: { self.sut.viewDidAppear() }) @@ -153,7 +155,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_description_is_available_after_fetching_product() { let product = Product.fixture(longDescription: "Product Description") - initViewModel(product: product) + initViewModel(productDetailsConfiguration: .product(product)) XCTAssertEmitsValue(from: sut.$state.drop(while: \.isLoading), afterTrigger: { self.sut.viewDidAppear() }) @@ -166,7 +168,7 @@ final class ProductDetailsViewModelTests: XCTestCase { } func test_price_type_is_not_nil_with_sale_product() { - initViewModel(product: Product.blazer) + initViewModel(productDetailsConfiguration: .product(Product.blazer)) guard case .sale(let fullPrice, let finalPrice) = sut.priceType else { XCTFail("Unexpected price type") return @@ -176,7 +178,7 @@ final class ProductDetailsViewModelTests: XCTestCase { } func test_price_type_is_not_nil_with_range_price_product() { - initViewModel(product: Product.hat) + initViewModel(productDetailsConfiguration: .product(Product.hat)) guard case .range(let lowerBound, let upperBound, let separator) = sut.priceType else { XCTFail("Unexpected price type") return @@ -187,7 +189,7 @@ final class ProductDetailsViewModelTests: XCTestCase { } func test_price_type_is_not_nil_with_default_price_product() { - initViewModel(product: Product.necklace) + initViewModel(productDetailsConfiguration: .product(Product.necklace)) guard case .default(let price) = sut.priceType else { XCTFail("Unexpected price type") return @@ -199,7 +201,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_is_fetched_when_view_appears() { let productId = "1" - initViewModel(productId: productId) + initViewModel(productDetailsConfiguration: .id(productId)) let expectation = expectation(description: "Wait for service call") mockProductService.onGetProductCalled = { id in @@ -214,7 +216,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_is_not_fetched_when_view_appears_if_already_fetched() { let productId = "1" - initViewModel(productId: productId) + initViewModel(productDetailsConfiguration: .id(productId)) let firstExpectation = expectation(description: "Wait for service call") let secondExpectation = expectation(description: "Wait for success state") @@ -336,7 +338,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_does_not_report_title_section_loading_when_placeholder_available() { let product = Product.fixture(name: "Product Name") - initViewModel(product: product) + initViewModel(productDetailsConfiguration: .product(product)) let result = sut.shouldShowLoading(for: .titleHeader) XCTAssertFalse(result) @@ -589,7 +591,7 @@ final class ProductDetailsViewModelTests: XCTestCase { let product = Product.fixture(name: "Product Name", defaultVariant: variant1, variants: [variant1, variant2]) - initViewModel(product: product) + initViewModel(productDetailsConfiguration: .product(product)) mockProductService.onGetProductCalled = { _ in product @@ -807,7 +809,8 @@ final class ProductDetailsViewModelTests: XCTestCase { brand: .fixture(name: "Product Brand"), variants: (expectedMatchedColors + expectedNonMatchedColors).map { Product.Variant.fixture(colour: $0) }) - initViewModel(product: product) + initViewModel(productDetailsConfiguration: .product(product)) + let allSwatches = sut.colorSelectionConfiguration.items let swatchesSearchResult = sut.colorSwatches(filteredBy: "Col") XCTAssertTrue(swatchesSearchResult.map(\.name).contains(expectedMatchedColors.map(\.name))) XCTAssertFalse(swatchesSearchResult.map(\.name).contains(expectedNonMatchedColors.map(\.name))) @@ -815,14 +818,7 @@ final class ProductDetailsViewModelTests: XCTestCase { // MARK: - Helper methods - private func initViewModel(productId: String = "", product: Product? = nil) { - let productKind: ThemedProductDetailsScreen - if let product = product { - productKind = .product(product) - } else { - productKind = .id(productId) - } - - sut = .init(productKind: productKind, dependencies: mockDependencies) + private func initViewModel(productDetailsConfiguration: ProductDetailsConfiguration = .id("")) { + sut = .init(productDetailsConfiguration: productDetailsConfiguration, dependencies: mockDependencies) } } diff --git a/Alfie/AlfieTests/Navigation/Deeplink/Handlers/DeepLinkHandlerTests.swift b/Alfie/AlfieTests/Navigation/Deeplink/Handlers/DeepLinkHandlerTests.swift index abe840c5..5001ae83 100644 --- a/Alfie/AlfieTests/Navigation/Deeplink/Handlers/DeepLinkHandlerTests.swift +++ b/Alfie/AlfieTests/Navigation/Deeplink/Handlers/DeepLinkHandlerTests.swift @@ -218,7 +218,7 @@ final class DeepLinkHandlerTests: XCTestCase { let expectation = expectation(description: "wait for coordinator call") mockCoordinator.onNavigateToScreenCalled = { screen in - XCTAssertEqual(screen, .productDetails(.id(productId))) + XCTAssertEqual(screen, .productDetails(configuration: .id(productId))) expectation.fulfill() } From 17c43477168c860d022061457d0d788257e22a45 Mon Sep 17 00:00:00 2001 From: JoaoPinhoMinder Date: Mon, 14 Apr 2025 09:42:41 +0100 Subject: [PATCH 6/6] ALFMOB-162: Code review --- Alfie/Alfie/Navigation/ViewFactory.swift | 2 +- .../ProductDetailsViewModel.swift | 4 +-- .../ProductDetailsViewModelTests.swift | 35 +++++++++---------- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/Alfie/Alfie/Navigation/ViewFactory.swift b/Alfie/Alfie/Navigation/ViewFactory.swift index ab10523d..855aa671 100644 --- a/Alfie/Alfie/Navigation/ViewFactory.swift +++ b/Alfie/Alfie/Navigation/ViewFactory.swift @@ -138,7 +138,7 @@ final class ViewFactory: ViewFactoryProtocol { case .productDetails(let configuration): ProductDetailsView( viewModel: ProductDetailsViewModel( - productDetailsConfiguration: configuration, + configuration: configuration, dependencies: ProductDetailsDependencyContainer( productService: serviceProvider.productService, webUrlProvider: serviceProvider.webUrlProvider, diff --git a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift index 7b7db47d..f3a89ba6 100644 --- a/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift +++ b/Alfie/Alfie/Views/ProductDetails/ProductDetailsViewModel.swift @@ -90,12 +90,12 @@ final class ProductDetailsViewModel: ProductDetailsViewModelProtocol { } init( - productDetailsConfiguration: ProductDetailsConfiguration, + configuration: ProductDetailsConfiguration, dependencies: ProductDetailsDependencyContainer ) { self.dependencies = dependencies - switch productDetailsConfiguration { + switch configuration { case .id(let productId): self.productId = productId self.initialSelectedProduct = nil diff --git a/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift b/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift index 3c59f1eb..979b43df 100644 --- a/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift +++ b/Alfie/AlfieTests/Features/ProductDetailsViewModelTests.swift @@ -53,7 +53,7 @@ final class ProductDetailsViewModelTests: XCTestCase { brand: .fixture(name: "Product Brand"), defaultVariant: variant, variants: [variant]) - initViewModel(productDetailsConfiguration: .product(product)) + initViewModel(configuration: .product(product)) XCTAssertEqual(sut.productName, product.name) XCTAssertEqual(sut.productTitle, product.brand.name) let colorSelectionConfiguration = sut.colorSelectionConfiguration @@ -73,7 +73,7 @@ final class ProductDetailsViewModelTests: XCTestCase { let product = Product.fixture(name: "Product Name", brand: .fixture(name: "Product Brand"), variants: [variant]) - initViewModel(productDetailsConfiguration: .product(product)) + initViewModel(configuration: .product(product)) XCTAssertEqual(sut.sizingSelectionConfiguration.items.count, 0) } @@ -84,7 +84,7 @@ final class ProductDetailsViewModelTests: XCTestCase { XCTAssertTrue(sut.productId.isEmpty) let productId = "1" - initViewModel(productDetailsConfiguration: .id(productId)) + initViewModel(configuration: .id(productId)) XCTAssertEqual(sut.productId, productId) } @@ -95,7 +95,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_title_is_available_after_fetching_product() { let product = Product.fixture(brand: .fixture(name: "Product Brand")) - initViewModel(productDetailsConfiguration: .product(product)) + initViewModel(configuration: .product(product)) XCTAssertEmitsValue(from: sut.$state.drop(while: \.isLoading), afterTrigger: { self.sut.viewDidAppear() }) @@ -109,7 +109,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_name_is_available_after_fetching_product() { let product = Product.fixture(name: "Product Name") - initViewModel(productDetailsConfiguration: .product(product)) + initViewModel(configuration: .product(product)) XCTAssertEmitsValue(from: sut.$state.drop(while: \.isLoading), afterTrigger: { self.sut.viewDidAppear() }) @@ -130,7 +130,7 @@ final class ProductDetailsViewModelTests: XCTestCase { let product = Product.fixture(name: "Product Name", defaultVariant: variant, variants: [variant]) - initViewModel(productDetailsConfiguration: .product(product)) + initViewModel(configuration: .product(product)) XCTAssertEmitsValue(from: sut.$state.drop(while: \.isLoading), afterTrigger: { self.sut.viewDidAppear() }) @@ -155,7 +155,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_description_is_available_after_fetching_product() { let product = Product.fixture(longDescription: "Product Description") - initViewModel(productDetailsConfiguration: .product(product)) + initViewModel(configuration: .product(product)) XCTAssertEmitsValue(from: sut.$state.drop(while: \.isLoading), afterTrigger: { self.sut.viewDidAppear() }) @@ -168,7 +168,7 @@ final class ProductDetailsViewModelTests: XCTestCase { } func test_price_type_is_not_nil_with_sale_product() { - initViewModel(productDetailsConfiguration: .product(Product.blazer)) + initViewModel(configuration: .product(Product.blazer)) guard case .sale(let fullPrice, let finalPrice) = sut.priceType else { XCTFail("Unexpected price type") return @@ -178,7 +178,7 @@ final class ProductDetailsViewModelTests: XCTestCase { } func test_price_type_is_not_nil_with_range_price_product() { - initViewModel(productDetailsConfiguration: .product(Product.hat)) + initViewModel(configuration: .product(Product.hat)) guard case .range(let lowerBound, let upperBound, let separator) = sut.priceType else { XCTFail("Unexpected price type") return @@ -189,7 +189,7 @@ final class ProductDetailsViewModelTests: XCTestCase { } func test_price_type_is_not_nil_with_default_price_product() { - initViewModel(productDetailsConfiguration: .product(Product.necklace)) + initViewModel(configuration: .product(Product.necklace)) guard case .default(let price) = sut.priceType else { XCTFail("Unexpected price type") return @@ -201,7 +201,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_is_fetched_when_view_appears() { let productId = "1" - initViewModel(productDetailsConfiguration: .id(productId)) + initViewModel(configuration: .id(productId)) let expectation = expectation(description: "Wait for service call") mockProductService.onGetProductCalled = { id in @@ -216,7 +216,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_product_is_not_fetched_when_view_appears_if_already_fetched() { let productId = "1" - initViewModel(productDetailsConfiguration: .id(productId)) + initViewModel(configuration: .id(productId)) let firstExpectation = expectation(description: "Wait for service call") let secondExpectation = expectation(description: "Wait for success state") @@ -338,7 +338,7 @@ final class ProductDetailsViewModelTests: XCTestCase { func test_does_not_report_title_section_loading_when_placeholder_available() { let product = Product.fixture(name: "Product Name") - initViewModel(productDetailsConfiguration: .product(product)) + initViewModel(configuration: .product(product)) let result = sut.shouldShowLoading(for: .titleHeader) XCTAssertFalse(result) @@ -591,7 +591,7 @@ final class ProductDetailsViewModelTests: XCTestCase { let product = Product.fixture(name: "Product Name", defaultVariant: variant1, variants: [variant1, variant2]) - initViewModel(productDetailsConfiguration: .product(product)) + initViewModel(configuration: .product(product)) mockProductService.onGetProductCalled = { _ in product @@ -809,8 +809,7 @@ final class ProductDetailsViewModelTests: XCTestCase { brand: .fixture(name: "Product Brand"), variants: (expectedMatchedColors + expectedNonMatchedColors).map { Product.Variant.fixture(colour: $0) }) - initViewModel(productDetailsConfiguration: .product(product)) - let allSwatches = sut.colorSelectionConfiguration.items + initViewModel(configuration: .product(product)) let swatchesSearchResult = sut.colorSwatches(filteredBy: "Col") XCTAssertTrue(swatchesSearchResult.map(\.name).contains(expectedMatchedColors.map(\.name))) XCTAssertFalse(swatchesSearchResult.map(\.name).contains(expectedNonMatchedColors.map(\.name))) @@ -818,7 +817,7 @@ final class ProductDetailsViewModelTests: XCTestCase { // MARK: - Helper methods - private func initViewModel(productDetailsConfiguration: ProductDetailsConfiguration = .id("")) { - sut = .init(productDetailsConfiguration: productDetailsConfiguration, dependencies: mockDependencies) + private func initViewModel(configuration: ProductDetailsConfiguration = .id("")) { + sut = .init(configuration: configuration, dependencies: mockDependencies) } }