From 78366cd4cfa60e29916c126dbb02d97f53a4f6da Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 31 Oct 2024 15:34:31 +0000 Subject: [PATCH 1/6] Start making ItemList generic over item types --- .../Classes/POS/POSDisplayableItem.swift | 41 +++++++++++++++++++ .../POS/PointOfSaleAggregateModel.swift | 24 +++++------ .../POS/PointOfSaleItemListState.swift | 12 +++--- .../POS/Presentation/ItemListView.swift | 16 +++----- .../POS/ViewModels/ItemListViewModel.swift | 10 +---- .../ItemListViewModelProtocol.swift | 2 - .../WooCommerce.xcodeproj/project.pbxproj | 4 ++ .../Yosemite/PointOfSale/POSProduct.swift | 31 +++++++++----- 8 files changed, 91 insertions(+), 49 deletions(-) create mode 100644 WooCommerce/Classes/POS/POSDisplayableItem.swift diff --git a/WooCommerce/Classes/POS/POSDisplayableItem.swift b/WooCommerce/Classes/POS/POSDisplayableItem.swift new file mode 100644 index 00000000000..8b151ba3b80 --- /dev/null +++ b/WooCommerce/Classes/POS/POSDisplayableItem.swift @@ -0,0 +1,41 @@ +import SwiftUI +import struct Yosemite.POSProduct +import protocol Yosemite.POSItem + +protocol POSDisplayableItem: Identifiable, Equatable { + var id: Int64 { get } + var item: POSItem { get } + associatedtype ItemView: View + @ViewBuilder var view: ItemView { get } +} + +struct POSProductItem: POSDisplayableItem { + var id: Int64 { + product.productID + } + var product: POSProduct + var item: POSItem { product } + let addItemToCart: (CartItem) -> Void + + init?(item: POSItem, addItemToCart: @escaping(CartItem) -> Void) { + guard let product = item as? POSProduct else { + return nil + } + self.product = product + self.addItemToCart = addItemToCart + } + + @ViewBuilder + var view: some View { + Button(action: { + let cartItem = CartItem(id: UUID(), item: product, quantity: 1) + addItemToCart(cartItem) + }, label: { + ItemCardView(item: product) + }) + } + + static func ==(lhs: POSProductItem, rhs: POSProductItem) -> Bool { + return lhs.product == rhs.product + } +} diff --git a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift index 33f741bc35d..f6664995eb0 100644 --- a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift @@ -18,7 +18,7 @@ final class PointOfSaleAggregateModel: ObservableObject { } @Published private(set) var orderStage: OrderStage = .building - @Published private(set) var allItems: [POSItem] = [] + @Published private(set) var allItems: [any POSDisplayableItem] = [] @Published private(set) var cart: [CartItem] = [] @Published private(set) var orderState: PointOfSaleOrderState = .idle @Published private(set) var order: Order? = nil @@ -70,7 +70,7 @@ final class PointOfSaleAggregateModel: ObservableObject { @MainActor func loadItems(pageNumber: Int) async { do { - itemListState = .loading + itemListState = .loading(allItems) try await fetchItems(pageNumber: pageNumber) } catch { itemListState = .error(PointOfSaleErrorState.errorOnLoadingProducts()) @@ -80,9 +80,14 @@ final class PointOfSaleAggregateModel: ObservableObject { @MainActor private func fetchItems(pageNumber: Int) async throws { let newItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber) - let uniqueNewItems = newItems.filter { newItem in - !allItems.contains(where: { $0.productID == newItem.productID }) - } + let uniqueNewItems = newItems + .filter { newItem in + !allItems.contains(where: { $0.id == newItem.productID }) + } + .compactMap { [weak self] item -> (any POSDisplayableItem)? in + guard let self else { return nil } + return POSProductItem(item: item, addItemToCart: addItemToCart(_:)) + } allItems.append(contentsOf: uniqueNewItems) @@ -115,14 +120,9 @@ final class PointOfSaleAggregateModel: ObservableObject { paymentState = .idle } - func selected(item: POSItem) { - let cartItem = CartItem(id: UUID(), item: item, quantity: 1) - addItemToCart(cartItem) - analytics.track(.pointOfSaleAddItemToCart) - } - func addItemToCart(_ item: CartItem) { cart.insert(item, at: 0) + analytics.track(.pointOfSaleAddItemToCart) } func removeItemFromCart(_ cartItem: CartItem) { @@ -136,7 +136,7 @@ final class PointOfSaleAggregateModel: ObservableObject { @MainActor func submitCart() async { orderStage = .finalizing - await startSyncingOrder(with: cart, allItems: allItems) + await startSyncingOrder(with: cart, allItems: allItems.map { $0.item }) } private func startSyncingOrder(with cartItems: [CartItem], allItems: [POSItem]) async { diff --git a/WooCommerce/Classes/POS/PointOfSaleItemListState.swift b/WooCommerce/Classes/POS/PointOfSaleItemListState.swift index a643c6f87e1..e7ca26fba36 100644 --- a/WooCommerce/Classes/POS/PointOfSaleItemListState.swift +++ b/WooCommerce/Classes/POS/PointOfSaleItemListState.swift @@ -3,19 +3,19 @@ import protocol Yosemite.POSItem enum PointOfSaleItemListState: Equatable { case empty case initialLoading - case loading - case loaded([POSItem]) + case loading(_ existingItems: [any POSDisplayableItem]) + case loaded([any POSDisplayableItem]) case error(PointOfSaleErrorState) // Equatable conformance for testing: static func == (lhs: PointOfSaleItemListState, rhs: PointOfSaleItemListState) -> Bool { switch (lhs, rhs) { case (.initialLoading, .initialLoading), - (.empty, .empty), - (.loading, .loading): + (.empty, .empty): return true - case (.loaded(let lhsItems), .loaded(let rhsItems)): - return lhsItems.map { $0.itemID } == rhsItems.map { $0.itemID } + case (.loading(let lhsItems), .loading(let rhsItems)), + (.loaded(let lhsItems), .loaded(let rhsItems)): + return true// lhsItems == rhsItems case (.error(let lhsError), .error(let rhsError)): return lhsError == rhsError default: diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index fcac501164c..6db213b95a5 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -24,8 +24,8 @@ struct ItemListView: View { // These cases are handled directly in the dashboard, we do not render // a specific view within the ItemListView to handle them EmptyView() - case .loading, .loaded: - listView(viewModel.items) + case .loading(let items), .loaded(let items): + listView(items) } } .refreshable { @@ -123,18 +123,14 @@ private extension ItemListView { } @ViewBuilder - func listView(_ items: [POSItem]) -> some View { + func listView(_ items: [any POSDisplayableItem]) -> some View { ScrollView { VStack { if dynamicTypeSize.isAccessibilitySize, viewModel.shouldShowHeaderBanner { bannerCardView } - ForEach(items, id: \.productID) { item in - Button(action: { - viewModel.select(item) - }, label: { - ItemCardView(item: item) - }) + ForEach(items, id: \.id) { item in + AnyView(item.view) } } .padding(.bottom, floatingControlAreaSize.height) @@ -142,7 +138,7 @@ private extension ItemListView { .background(GeometryReader { proxy in Color.clear .onChange(of: proxy.frame(in: .global).maxY) { maxY in - if posModel.itemListState == .loading { + if case .loading = posModel.itemListState { return } let viewHeight = UIScreen.main.bounds.height diff --git a/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift b/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift index d8455c4db9c..f66051077d9 100644 --- a/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift @@ -5,10 +5,6 @@ import protocol Yosemite.POSItem final class ItemListViewModel: ItemListViewModelProtocol { let posModel: PointOfSaleAggregateModel - var items: [POSItem] { - posModel.allItems - } - @Published private(set) var isHeaderBannerDismissed: Bool = false @Published var showSimpleProductsModal: Bool = false @@ -19,17 +15,13 @@ final class ItemListViewModel: ItemListViewModelProtocol { if UserDefaults.standard.bool(forKey: BannerState.isSimpleProductsOnlyBannerDismissedKey) == true { return false } - return !isHeaderBannerDismissed && items.isNotEmpty + return !isHeaderBannerDismissed && posModel.allItems.isNotEmpty } init(posModel: PointOfSaleAggregateModel) { self.posModel = posModel } - func select(_ item: POSItem) { - posModel.selected(item: item) - } - @MainActor func loadNextItems() async { // TODO: Optimize API calls. gh-14186 diff --git a/WooCommerce/Classes/POS/ViewModels/ItemListViewModelProtocol.swift b/WooCommerce/Classes/POS/ViewModels/ItemListViewModelProtocol.swift index a72f07f6d3b..be556d658b5 100644 --- a/WooCommerce/Classes/POS/ViewModels/ItemListViewModelProtocol.swift +++ b/WooCommerce/Classes/POS/ViewModels/ItemListViewModelProtocol.swift @@ -3,11 +3,9 @@ import Foundation import protocol Yosemite.POSItem protocol ItemListViewModelProtocol: ObservableObject { - var items: [POSItem] { get } var isHeaderBannerDismissed: Bool { get } var shouldShowHeaderBanner: Bool { get } - func select(_ item: POSItem) func loadNextItems() async func dismissBanner() } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 392bb2b216d..e2617c860b8 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -831,6 +831,7 @@ 20B7E3152CD0DDDA007FD997 /* PointOfSaleOrderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B7E3142CD0DDDA007FD997 /* PointOfSaleOrderState.swift */; }; 20B7E3172CD100DC007FD997 /* PointOfSalePaymentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B7E3162CD100DC007FD997 /* PointOfSalePaymentState.swift */; }; 20B7E3192CD13CE3007FD997 /* PointOfSaleItemListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B7E3182CD13CE3007FD997 /* PointOfSaleItemListState.swift */; }; + 20B7E3512CD3DAB5007FD997 /* POSDisplayableItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B7E3502CD3DAB5007FD997 /* POSDisplayableItem.swift */; }; 20BBD62C2B3060A300A903F6 /* AddOrderComponentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBD62B2B3060A300A903F6 /* AddOrderComponentsSection.swift */; }; 20BCF6EE2B0E478B00954840 /* WooPaymentsDepositsOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6ED2B0E478B00954840 /* WooPaymentsDepositsOverviewViewModel.swift */; }; 20BCF6F02B0E48CC00954840 /* WooPaymentsDepositsOverviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BCF6EF2B0E48CC00954840 /* WooPaymentsDepositsOverviewViewModelTests.swift */; }; @@ -3922,6 +3923,7 @@ 20B7E3142CD0DDDA007FD997 /* PointOfSaleOrderState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderState.swift; sourceTree = ""; }; 20B7E3162CD100DC007FD997 /* PointOfSalePaymentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSalePaymentState.swift; sourceTree = ""; }; 20B7E3182CD13CE3007FD997 /* PointOfSaleItemListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleItemListState.swift; sourceTree = ""; }; + 20B7E3502CD3DAB5007FD997 /* POSDisplayableItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSDisplayableItem.swift; sourceTree = ""; }; 20BBD62B2B3060A300A903F6 /* AddOrderComponentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOrderComponentsSection.swift; sourceTree = ""; }; 20BCF6ED2B0E478B00954840 /* WooPaymentsDepositsOverviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverviewViewModel.swift; sourceTree = ""; }; 20BCF6EF2B0E48CC00954840 /* WooPaymentsDepositsOverviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsDepositsOverviewViewModelTests.swift; sourceTree = ""; }; @@ -7101,6 +7103,7 @@ 026826972BF59D9E0036F959 /* Utils */, 026826A12BF59DED0036F959 /* Presentation */, 20B7E3122CCFFEB4007FD997 /* PointOfSaleAggregateModel.swift */, + 20B7E3502CD3DAB5007FD997 /* POSDisplayableItem.swift */, 20B7E3182CD13CE3007FD997 /* PointOfSaleItemListState.swift */, 20B7E3162CD100DC007FD997 /* PointOfSalePaymentState.swift */, 20B7E3142CD0DDDA007FD997 /* PointOfSaleOrderState.swift */, @@ -14731,6 +14734,7 @@ 31FE28C225E6D338003519F2 /* LearnMoreTableViewCell.swift in Sources */, 02D45647231CB1FB008CF0A9 /* UIImage+Dot.swift in Sources */, 680BA59A2A4C377900F5559D /* UpgradeViewState.swift in Sources */, + 20B7E3512CD3DAB5007FD997 /* POSDisplayableItem.swift in Sources */, E11228BE2707267F004E9F2D /* CardPresentModalUpdateFailedNonRetryable.swift in Sources */, 86DE68822B4BA47A00B437A6 /* BlazeAdDestinationSettingViewModel.swift in Sources */, EEC5E01129A70CC300416CAC /* StoreSetupProgressView.swift in Sources */, diff --git a/Yosemite/Yosemite/PointOfSale/POSProduct.swift b/Yosemite/Yosemite/PointOfSale/POSProduct.swift index 49121055a15..5c1af3609d0 100644 --- a/Yosemite/Yosemite/PointOfSale/POSProduct.swift +++ b/Yosemite/Yosemite/PointOfSale/POSProduct.swift @@ -1,14 +1,14 @@ -struct POSProduct: POSItem { - let itemID: UUID - let productID: Int64 - let name: String - let price: String - let formattedPrice: String - let itemCategories: [String] - var productImageSource: String? - let productType: ProductType +public struct POSProduct: POSItem, Equatable { + public let itemID: UUID + public let productID: Int64 + public let name: String + public let price: String + public let formattedPrice: String + public let itemCategories: [String] + public var productImageSource: String? + public let productType: ProductType - init(itemID: UUID, + public init(itemID: UUID, productID: Int64, name: String, price: String, @@ -25,4 +25,15 @@ struct POSProduct: POSItem { self.productImageSource = productImageSource self.productType = productType } + + public static func ==(lhs: POSProduct, rhs: POSProduct) -> Bool { + return lhs.itemID == rhs.itemID && + lhs.productID == rhs.productID && + lhs.name == rhs.name && + lhs.price == rhs.price && + lhs.formattedPrice == rhs.formattedPrice && + lhs.itemCategories == rhs.itemCategories && + lhs.productImageSource == rhs.productImageSource && + lhs.productType == rhs.productType + } } From de495654d18990c8a2ea6326b7408dd6395b11a0 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 31 Oct 2024 16:05:04 +0000 Subject: [PATCH 2/6] Add a discount to the item list --- .../Classes/POS/POSDisplayableItem.swift | 97 ++++++++++++++++++- .../POS/PointOfSaleAggregateModel.swift | 17 +++- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/POS/POSDisplayableItem.swift b/WooCommerce/Classes/POS/POSDisplayableItem.swift index 8b151ba3b80..41d091a712c 100644 --- a/WooCommerce/Classes/POS/POSDisplayableItem.swift +++ b/WooCommerce/Classes/POS/POSDisplayableItem.swift @@ -1,17 +1,18 @@ import SwiftUI import struct Yosemite.POSProduct import protocol Yosemite.POSItem +import enum Yosemite.ProductType protocol POSDisplayableItem: Identifiable, Equatable { - var id: Int64 { get } + var id: UUID { get } var item: POSItem { get } associatedtype ItemView: View @ViewBuilder var view: ItemView { get } } struct POSProductItem: POSDisplayableItem { - var id: Int64 { - product.productID + var id: UUID { + product.itemID } var product: POSProduct var item: POSItem { product } @@ -39,3 +40,93 @@ struct POSProductItem: POSDisplayableItem { return lhs.product == rhs.product } } + +struct POSDiscount: POSItem, Equatable { + var itemID = UUID() + + var productID: Int64 = 0 + + var name: String = "A fixed discount" + + var price: String = "-5.00" + + var formattedPrice: String = "-$5.00" + + var itemCategories: [String] = [] + + var productImageSource: String? = nil + + var productType: Yosemite.ProductType = .simple +} + +struct POSDiscountItem: POSDisplayableItem { + var id: UUID { discount.itemID } + let discount: POSDiscount + var item: POSItem { discount } + + @ScaledMetric private var scale: CGFloat = 1.0 + + init?(item: POSItem) { + guard let discount = item as? POSDiscount else { + return nil + } + self.discount = discount + } + + @ViewBuilder + var view: some View { + HStack(spacing: Constants.cardSpacing) { + Rectangle() + .overlay { + Image(systemName: "basket") + } + .frame(width: min(Constants.productCardSize * scale, Constants.maximumProductCardSize), + height: Constants.productCardSize * scale) + .foregroundColor(Color(.secondarySystemFill)) + + DynamicHStack(spacing: Constants.textSpacing) { + Text(item.name) + .lineLimit(2) + .foregroundStyle(Color.posPrimaryText) + .multilineTextAlignment(.leading) + .font(Constants.itemNameFont) + Spacer() + Text(item.formattedPrice) + .foregroundStyle(Color.posPrimaryText) + .font(Constants.itemPriceFont) + } + .padding(.horizontal, Constants.horizontalTextPadding * (1 / scale)) + .padding(.vertical, Constants.verticalTextPadding * (1 / scale)) + Spacer() + } + .frame(maxWidth: .infinity, idealHeight: Constants.productCardSize * scale) + .background(Color.posSecondaryBackground) + .overlay { + RoundedRectangle(cornerRadius: Constants.productCardCornerRadius) + .stroke(Color.black, lineWidth: Constants.nilOutline) + } + .clipShape(RoundedRectangle(cornerRadius: Constants.productCardCornerRadius)) + .shadow(color: Color.black.opacity(0.08), radius: 4, y: 2) + } + + static func ==(lhs: POSDiscountItem, rhs: POSDiscountItem) -> Bool { + return lhs.discount == rhs.discount + } +} + +private extension POSDiscountItem { + enum Constants { + static let productCardSize: CGFloat = 112 + static let maximumProductCardSize: CGFloat = Constants.productCardSize * 2 + static let productCardCornerRadius: CGFloat = 8 + // The use of stroke means the shape is rendered as an outline (border) rather than a filled shape, + // since we still have to give it a value, we use 0 so it renders no border but it's shaped as one. + static let nilOutline: CGFloat = 0 + static let cardSpacing: CGFloat = 0 + static let textSpacing: CGFloat = 8 + static let horizontalTextPadding: CGFloat = 32 + static let verticalTextPadding: CGFloat = 8 + static let itemNameFont: POSFontStyle = .posBodyEmphasized + static let itemPriceFont: POSFontStyle = .posBodyRegular + } +} diff --git a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift index f6664995eb0..d34f2b44052 100644 --- a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift @@ -4,6 +4,7 @@ import Combine import protocol Yosemite.POSOrderServiceProtocol import protocol Yosemite.POSItem import protocol Yosemite.POSItemProvider +import struct Yosemite.POSProduct import struct Yosemite.Order import struct Yosemite.OrderItem import struct Yosemite.POSCartItem @@ -79,14 +80,24 @@ final class PointOfSaleAggregateModel: ObservableObject { @MainActor private func fetchItems(pageNumber: Int) async throws { - let newItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber) + var newItems = try await itemProvider.providePointOfSaleItems(pageNumber: pageNumber) + if pageNumber == 1 { + newItems.insert(POSDiscount(), at: 0) + } let uniqueNewItems = newItems .filter { newItem in - !allItems.contains(where: { $0.id == newItem.productID }) + !allItems.contains(where: { $0.id == newItem.itemID }) } .compactMap { [weak self] item -> (any POSDisplayableItem)? in guard let self else { return nil } - return POSProductItem(item: item, addItemToCart: addItemToCart(_:)) + switch item { + case is POSProduct: + return POSProductItem(item: item, addItemToCart: addItemToCart(_:)) + case is POSDiscount: + return POSDiscountItem(item: item) + default: + return nil + } } allItems.append(contentsOf: uniqueNewItems) From 790f6f80380c2fad3fc8b4ffd3554fb19aca0dd2 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 31 Oct 2024 16:24:47 +0000 Subject: [PATCH 3/6] Make itemslist private --- WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift | 2 +- WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift index d34f2b44052..4054252456e 100644 --- a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift @@ -19,7 +19,7 @@ final class PointOfSaleAggregateModel: ObservableObject { } @Published private(set) var orderStage: OrderStage = .building - @Published private(set) var allItems: [any POSDisplayableItem] = [] + private var allItems: [any POSDisplayableItem] = [] @Published private(set) var cart: [CartItem] = [] @Published private(set) var orderState: PointOfSaleOrderState = .idle @Published private(set) var order: Order? = nil diff --git a/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift b/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift index f66051077d9..1fc96e30dd8 100644 --- a/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/ItemListViewModel.swift @@ -15,7 +15,7 @@ final class ItemListViewModel: ItemListViewModelProtocol { if UserDefaults.standard.bool(forKey: BannerState.isSimpleProductsOnlyBannerDismissedKey) == true { return false } - return !isHeaderBannerDismissed && posModel.allItems.isNotEmpty + return !isHeaderBannerDismissed && posModel.itemListState != .empty } init(posModel: PointOfSaleAggregateModel) { From a94b138d846c5a8aa5a2431999101b0e2e93d780 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 31 Oct 2024 17:29:42 +0000 Subject: [PATCH 4/6] Make the items views --- WooCommerce/Classes/POS/POSDisplayableItem.swift | 10 +++------- .../Classes/POS/Presentation/ItemListView.swift | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/WooCommerce/Classes/POS/POSDisplayableItem.swift b/WooCommerce/Classes/POS/POSDisplayableItem.swift index 41d091a712c..7595ca3698b 100644 --- a/WooCommerce/Classes/POS/POSDisplayableItem.swift +++ b/WooCommerce/Classes/POS/POSDisplayableItem.swift @@ -3,11 +3,9 @@ import struct Yosemite.POSProduct import protocol Yosemite.POSItem import enum Yosemite.ProductType -protocol POSDisplayableItem: Identifiable, Equatable { +protocol POSDisplayableItem: View, Identifiable, Equatable { var id: UUID { get } var item: POSItem { get } - associatedtype ItemView: View - @ViewBuilder var view: ItemView { get } } struct POSProductItem: POSDisplayableItem { @@ -26,8 +24,7 @@ struct POSProductItem: POSDisplayableItem { self.addItemToCart = addItemToCart } - @ViewBuilder - var view: some View { + var body: some View { Button(action: { let cartItem = CartItem(id: UUID(), item: product, quantity: 1) addItemToCart(cartItem) @@ -73,8 +70,7 @@ struct POSDiscountItem: POSDisplayableItem { self.discount = discount } - @ViewBuilder - var view: some View { + var body: some View { HStack(spacing: Constants.cardSpacing) { Rectangle() .overlay { diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index 6db213b95a5..7a747a24334 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -130,7 +130,7 @@ private extension ItemListView { bannerCardView } ForEach(items, id: \.id) { item in - AnyView(item.view) + AnyView(item) } } .padding(.bottom, floatingControlAreaSize.height) From 1a93d763c61a401e74b18647c803d1f1a9b2f5b4 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 1 Nov 2024 08:29:31 +0000 Subject: [PATCH 5/6] Inject posModel using environmentObject --- WooCommerce/Classes/POS/POSDisplayableItem.swift | 7 +++---- WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift | 2 +- .../POS/Presentation/PointOfSaleEntryPointView.swift | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WooCommerce/Classes/POS/POSDisplayableItem.swift b/WooCommerce/Classes/POS/POSDisplayableItem.swift index 7595ca3698b..3766256169f 100644 --- a/WooCommerce/Classes/POS/POSDisplayableItem.swift +++ b/WooCommerce/Classes/POS/POSDisplayableItem.swift @@ -14,20 +14,19 @@ struct POSProductItem: POSDisplayableItem { } var product: POSProduct var item: POSItem { product } - let addItemToCart: (CartItem) -> Void + @EnvironmentObject var posModel: PointOfSaleAggregateModel - init?(item: POSItem, addItemToCart: @escaping(CartItem) -> Void) { + init?(item: POSItem) { guard let product = item as? POSProduct else { return nil } self.product = product - self.addItemToCart = addItemToCart } var body: some View { Button(action: { let cartItem = CartItem(id: UUID(), item: product, quantity: 1) - addItemToCart(cartItem) + posModel.addItemToCart(cartItem) }, label: { ItemCardView(item: product) }) diff --git a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift index 4054252456e..72263f03054 100644 --- a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift @@ -92,7 +92,7 @@ final class PointOfSaleAggregateModel: ObservableObject { guard let self else { return nil } switch item { case is POSProduct: - return POSProductItem(item: item, addItemToCart: addItemToCart(_:)) + return POSProductItem(item: item) case is POSDiscount: return POSDiscountItem(item: item) default: diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index e87855a4fe1..055757fe9a1 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -53,6 +53,7 @@ struct PointOfSaleEntryPointView: View { itemListViewModel: itemListViewModel, posModel: posModel) .environmentObject(posModalManager) + .environmentObject(posModel) .onAppear { onPointOfSaleModeActiveStateChange(true) } From a7215c0686a4b239b34f85b3e01eab20f51e63f7 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 1 Nov 2024 09:23:24 +0000 Subject: [PATCH 6/6] Move item creation out of model --- WooCommerce/Classes/POS/POSDisplayableItem.swift | 11 +++++++++++ .../Classes/POS/PointOfSaleAggregateModel.swift | 12 +----------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/POS/POSDisplayableItem.swift b/WooCommerce/Classes/POS/POSDisplayableItem.swift index 3766256169f..56789507163 100644 --- a/WooCommerce/Classes/POS/POSDisplayableItem.swift +++ b/WooCommerce/Classes/POS/POSDisplayableItem.swift @@ -8,6 +8,17 @@ protocol POSDisplayableItem: View, Identifiable, Equatable { var item: POSItem { get } } +func createPOSDisplayableItem(for item: POSItem) -> (any POSDisplayableItem)? { + switch item { + case is POSProduct: + return POSProductItem(item: item) + case is POSDiscount: + return POSDiscountItem(item: item) + default: + return nil + } +} + struct POSProductItem: POSDisplayableItem { var id: UUID { product.itemID diff --git a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift index 72263f03054..79e28d1c503 100644 --- a/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/PointOfSaleAggregateModel.swift @@ -88,17 +88,7 @@ final class PointOfSaleAggregateModel: ObservableObject { .filter { newItem in !allItems.contains(where: { $0.id == newItem.itemID }) } - .compactMap { [weak self] item -> (any POSDisplayableItem)? in - guard let self else { return nil } - switch item { - case is POSProduct: - return POSProductItem(item: item) - case is POSDiscount: - return POSDiscountItem(item: item) - default: - return nil - } - } + .compactMap(createPOSDisplayableItem(for:)) allItems.append(contentsOf: uniqueNewItems)