Skip to content

Add option to fetch lightweight objects to reduce app hangs #15774

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: trunk
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,16 @@ public final class FetchResultSnapshotsProvider<MutableType: FetchResultSnapshot
return nil
}
}

/// The lightweight version of `object(withID:)` method. Returns readonly objects without relationships.
///
public func lightweightObject(withID objectID: FetchResultSnapshotObjectID) -> MutableType.ReadOnlyType? {
if let storageOrder = storage.loadObject(ofType: MutableType.self, with: objectID) {
return storageOrder.toLightweightReadOnly()
} else {
return nil
}
}
}

// MARK: - FetchedResultsController Activation
Expand Down
10 changes: 10 additions & 0 deletions Modules/Sources/Yosemite/Tools/ReadOnlyConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}


Expand All @@ -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()
}
}
17 changes: 17 additions & 0 deletions Modules/Sources/Yosemite/Tools/ResultsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ public class ResultsController<T: ResultsControllerMutableType> {
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.
///
Expand Down Expand Up @@ -203,6 +209,17 @@ public class ResultsController<T: ResultsControllerMutableType> {
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] {
Expand Down
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ final class OrderDetailsResultsControllers {
/// Products from an Order
///
var products: [Product] {
return productResultsController.fetchedObjects
return productResultsController.lightweightFetchedObjects
}

/// ProductVariations from an Order
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ private extension DashboardViewModel {
return
}

guard ordersResultsController.fetchedObjects.isEmpty else {
guard ordersResultsController.lightweightFetchedObjects.isEmpty else {
hasOrders = true
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}

Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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)
Expand Down