diff --git a/Modules/Sources/Yosemite/Model/Storage/Order+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Storage/Order+ReadOnlyConvertible.swift index 7f1338eefde..5090620f893 100644 --- a/Modules/Sources/Yosemite/Model/Storage/Order+ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Model/Storage/Order+ReadOnlyConvertible.swift @@ -123,6 +123,51 @@ extension Storage.Order: ReadOnlyConvertible { } + /// Returns a lightweight ReadOnly version of the receiver without any relationships populated. + /// + public func toLightweightReadOnly() -> Yosemite.Order { + Order(siteID: siteID, + orderID: orderID, + parentID: parentID, + customerID: customerID, + orderKey: orderKey, + isEditable: isEditable, + needsPayment: needsPayment, + needsProcessing: needsPayment, + number: number ?? "", + status: OrderStatusEnum(rawValue: statusKey), + currency: currency ?? "", + currencySymbol: "", // Not stored in the Storage Layer, only used in the notifications extension. + customerNote: customerNote ?? "", + dateCreated: dateCreated ?? Date(), + dateModified: dateModified ?? Date(), + datePaid: datePaid, + discountTotal: discountTotal ?? "", + discountTax: discountTax ?? "", + shippingTotal: shippingTotal ?? "", + shippingTax: shippingTax ?? "", + total: total ?? "", + totalTax: totalTax ?? "", + paymentMethodID: paymentMethodID ?? "", + paymentMethodTitle: paymentMethodTitle ?? "", + paymentURL: paymentURL as URL?, + chargeID: chargeID, + items: [], + billingAddress: createReadOnlyBillingAddress(), + shippingAddress: createReadOnlyShippingAddress(), + shippingLines: [], + coupons: [], + refunds: [], + fees: [], + taxes: [], + customFields: [], + renewalSubscriptionID: renewalSubscriptionID, + appliedGiftCards: [], + attributionInfo: attributionInfo?.toReadOnly(), + shippingLabels: []) + + } + // MARK: - Private Helpers diff --git a/Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift index 65ddd38c5a9..86f91a97ce8 100644 --- a/Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift @@ -200,6 +200,93 @@ extension Storage.Product: ReadOnlyConvertible { customFields: productCustomFields.sorted { $0.metadataID < $1.metadataID }) } + /// Returns a lightweight ReadOnly version of the receiver. + /// No relationships are populated except for images. + /// + public func toLightweightReadOnly() -> Yosemite.Product { + + let productImages = imagesArray.map { $0.toReadOnly() } + + return Product(siteID: siteID, + productID: productID, + name: name, + slug: slug, + permalink: permalink, + date: date ?? Date(timeIntervalSince1970: 0), + dateCreated: dateCreated ?? Date(timeIntervalSince1970: 0), + dateModified: dateModified, + dateOnSaleStart: dateOnSaleStart, + dateOnSaleEnd: dateOnSaleEnd, + productTypeKey: productTypeKey, + statusKey: statusKey, + featured: featured, + catalogVisibilityKey: catalogVisibilityKey, + fullDescription: fullDescription, + shortDescription: briefDescription, + sku: sku, + globalUniqueID: globalUniqueID, + price: price, + regularPrice: regularPrice, + salePrice: salePrice, + onSale: onSale, + purchasable: purchasable, + totalSales: Int(totalSales), + virtual: virtual, + downloadable: downloadable, + downloads: [], + downloadLimit: downloadLimit, + downloadExpiry: downloadExpiry, + buttonText: buttonText, + externalURL: externalURL, + taxStatusKey: taxStatusKey, + taxClass: taxClass, + manageStock: manageStock, + stockQuantity: nil, + stockStatusKey: stockStatusKey, + backordersKey: backordersKey, + backordersAllowed: backordersAllowed, + backordered: backordered, + soldIndividually: soldIndividually, + weight: weight, + dimensions: createReadOnlyDimensions(), + shippingRequired: shippingRequired, + shippingTaxable: shippingTaxable, + shippingClass: shippingClass, + shippingClassID: shippingClassID, + productShippingClass: nil, + reviewsAllowed: reviewsAllowed, + averageRating: averageRating, + ratingCount: Int(ratingCount), + relatedIDs: convertIDArray(relatedIDs), + upsellIDs: convertIDArray(upsellIDs), + crossSellIDs: convertIDArray(crossSellIDs), + parentID: parentID, + purchaseNote: purchaseNote, + categories: [], + tags: [], + images: productImages, + attributes: [], + defaultAttributes: [], + variations: variations ?? [], + groupedProducts: groupedProducts ?? [], + menuOrder: Int(menuOrder), + addOns: [], + isSampleItem: isSampleItem, + bundleStockStatus: nil, + bundleStockQuantity: bundleStockQuantity as? Int64, + bundleMinSize: bundleMinSize?.decimalValue, + bundleMaxSize: bundleMaxSize?.decimalValue, + bundledItems: [], + password: password, + compositeComponents: [], + subscription: subscription?.toReadOnly(), + minAllowedQuantity: minAllowedQuantity, + maxAllowedQuantity: maxAllowedQuantity, + groupOfQuantity: groupOfQuantity, + combineVariationQuantities: combineVariationQuantities?.boolValue, + customFields: []) + } + // MARK: - Private Helpers private func createReadOnlyDimensions() -> Yosemite.ProductDimensions { diff --git a/Modules/Sources/Yosemite/SnapshotsProvider/FetchResultSnapshotsProvider.swift b/Modules/Sources/Yosemite/SnapshotsProvider/FetchResultSnapshotsProvider.swift index c3d4dca8b70..94fd5126f5d 100644 --- a/Modules/Sources/Yosemite/SnapshotsProvider/FetchResultSnapshotsProvider.swift +++ b/Modules/Sources/Yosemite/SnapshotsProvider/FetchResultSnapshotsProvider.swift @@ -179,6 +179,16 @@ public final class FetchResultSnapshotsProvider MutableType.ReadOnlyType? { + if let storageOrder = storage.loadObject(ofType: MutableType.self, with: objectID) { + return storageOrder.toLightweightReadOnly() + } else { + return nil + } + } } // MARK: - FetchedResultsController Activation diff --git a/Modules/Sources/Yosemite/Tools/ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Tools/ReadOnlyConvertible.swift index ed088bea7e6..ab33955ce12 100644 --- a/Modules/Sources/Yosemite/Tools/ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Tools/ReadOnlyConvertible.swift @@ -17,6 +17,10 @@ public protocol ReadOnlyConvertible: TypeErasedReadOnlyConvertible { /// Returns a ReadOnly version of the receiver. /// func toReadOnly() -> ReadOnlyType + + /// Returns a lightweight ReadOnly version of the receiver. + /// + func toLightweightReadOnly() -> ReadOnlyType } @@ -40,4 +44,10 @@ extension ReadOnlyConvertible { public func toTypeErasedReadOnly() -> Any { return toReadOnly() } + + /// Default implementation is the same result with `toReadOnly()` + /// + public func toLightweightReadOnly() -> ReadOnlyType { + toReadOnly() + } } diff --git a/Modules/Sources/Yosemite/Tools/ResultsController.swift b/Modules/Sources/Yosemite/Tools/ResultsController.swift index ee9ea7b1992..a1405e6ae1f 100644 --- a/Modules/Sources/Yosemite/Tools/ResultsController.swift +++ b/Modules/Sources/Yosemite/Tools/ResultsController.swift @@ -143,6 +143,12 @@ public class ResultsController { return controller.object(at: indexPath).toReadOnly() } + /// Returns the lightweight fetched object at a given indexPath. + /// + public func lightweightObject(at indexPath: IndexPath) -> T.ReadOnlyType { + return controller.object(at: indexPath).toLightweightReadOnly() + } + /// Returns the fetched object at the given `indexPath`. Returns `nil` if the `indexPath` /// does not exist. /// @@ -203,6 +209,17 @@ public class ResultsController { return readOnlyObjects ?? [] } + /// Returns an array of all of the (ReadOnly) Fetched Objects with no relationships populated. + /// Suitable for list views. + /// + public var lightweightFetchedObjects: [T.ReadOnlyType] { + let readOnlyObjects = controller.fetchedObjects?.compactMap { mutableObject in + mutableObject.toLightweightReadOnly() + } + + return readOnlyObjects ?? [] + } + /// Returns an array of SectionInfo Entitites. /// public var sections: [SectionInfo] { diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 992325b5210..47351391263 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -5,6 +5,7 @@ ----- - [*] Order Details: Fix crash when reloading data [https://github.com/woocommerce/woocommerce-ios/pull/15764] - [*] Shipping Labels: Improved shipment management UI by hiding remove/merge options instead of disabling them, hiding merge option for orders with 2 or fewer unfulfilled shipments, and hiding the ellipsis menu when no remove/merge actions are available [https://github.com/woocommerce/woocommerce-ios/pull/15760] +- [*] Improve app performance by fetching lightweight objects when necessary [https://github.com/woocommerce/woocommerce-ios/pull/15774] - [**] POS: a POS tab in the tab bar is now available in the app for stores eligible for Point of Sale, instead of the previous entry point in the Menu tab. [https://github.com/woocommerce/woocommerce-ios/pull/15766] - [***] POS: Barcode scanning using Bluetooth scanners in Point of Sale (HID-based scanners are supported) [https://github.com/woocommerce/woocommerce-ios/pull/15776] - [*] POS: Support refreshing product list when it's empty [https://github.com/woocommerce/woocommerce-ios/pull/15782] diff --git a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsResultsControllers.swift b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsResultsControllers.swift index 4270b4f4934..68d25ea106e 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsResultsControllers.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsResultsControllers.swift @@ -90,7 +90,7 @@ final class OrderDetailsResultsControllers { /// Products from an Order /// var products: [Product] { - return productResultsController.fetchedObjects + return productResultsController.lightweightFetchedObjects } /// ProductVariations from an Order diff --git a/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignCreationFormViewModel.swift b/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignCreationFormViewModel.swift index 4ead8cd5fbf..dbead3f2780 100644 --- a/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignCreationFormViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignCreationFormViewModel.swift @@ -204,7 +204,7 @@ final class BlazeCampaignCreationFormViewModel: ObservableObject { private let storage: StorageManagerType private var product: Product? { - guard let product = productsResultsController.fetchedObjects.first else { + guard let product = productsResultsController.lightweightFetchedObjects.first else { assertionFailure("Unable to fetch product with ID: \(productID)") return nil } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift index df7015b71f7..f1c5955c706 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewModel.swift @@ -540,7 +540,7 @@ private extension DashboardViewModel { return } - guard ordersResultsController.fetchedObjects.isEmpty else { + guard ordersResultsController.lightweightFetchedObjects.isEmpty else { hasOrders = true return } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsDataSource.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsDataSource.swift index ff37009fa74..a76a80339b4 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsDataSource.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsDataSource.swift @@ -32,7 +32,7 @@ final class DefaultWooShippingItemsDataSource: WooShippingItemsDataSource { /// private var products: [Product] { try? productResultsController.performFetch() - return productResultsController.fetchedObjects + return productResultsController.lightweightFetchedObjects } /// Stored product variations that match the items in the order. diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift index e99b889f45e..59850dcb40c 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrderListViewModel.swift @@ -81,7 +81,7 @@ final class OrderListViewModel { /// private var hasAnyPublishedProducts: Bool { (storageManager.viewStorage.loadProducts(siteID: siteID) ?? []) - .map { $0.toReadOnly() } + .map { $0.toLightweightReadOnly() } .contains(where: { $0.productStatus == .published }) } @@ -326,7 +326,7 @@ extension OrderListViewModel { /// Creates an `OrderListCellViewModel` for the `Order` pointed to by `objectID`. func cellViewModel(withID objectID: FetchResultSnapshotObjectID) -> OrderListCellViewModel? { - guard let order = snapshotsProvider.object(withID: objectID) else { + guard let order = snapshotsProvider.lightweightObject(withID: objectID) else { return nil } diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift index 502a836dc19..046a35554f7 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift @@ -299,7 +299,7 @@ final class ProductsViewController: UIViewController, GhostableViewController { /// Selects the first product if one is available. Invoked when no product is selected when data is loaded in split view expanded mode. func selectFirstProductIfAvailable() { - guard let firstProduct = resultsController.fetchedObjects.first else { + guard let firstProduct = resultsController.lightweightFetchedObjects.first else { return } didSelectProduct(product: firstProduct) @@ -1026,7 +1026,7 @@ private extension ProductsViewController { .map { $0.productOrVariationID.id } var indexPathsToReload: [IndexPath] = [] - for (index, object) in resultsController.fetchedObjects.enumerated() { + for (index, object) in resultsController.lightweightFetchedObjects.enumerated() { if activeUploadIds.contains(object.productID) != oldIDs.contains(object.productID) { indexPathsToReload.append(IndexPath(row: index, section: 0)) } @@ -1114,7 +1114,7 @@ extension ProductsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(ProductsTabProductTableViewCell.self, for: indexPath) - let product = resultsController.object(at: indexPath) + let product = resultsController.lightweightObject(at: indexPath) let hasPendingUploads = activeUploadIds.contains(where: { $0 == product.productID }) let viewModel = ProductsTabProductViewModel(product: product, hasPendingUploads: hasPendingUploads)