Skip to content

Commit 89a0923

Browse files
authored
Order Details: Load simplified objects for products (#15959)
2 parents ad99355 + d27eb7b commit 89a0923

File tree

23 files changed

+280
-114
lines changed

23 files changed

+280
-114
lines changed

Modules/Sources/Yosemite/Internal/NSOrderedSet+Array.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
extension NSOrderedSet {
4-
func toArray<T>() -> [T] {
4+
public func toArray<T>() -> [T] {
55
guard let array = array as? [T] else {
66
return []
77
}

Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import Storage
33

44
extension Storage.Product {
5-
var imagesArray: [Storage.ProductImage] {
5+
public var imagesArray: [Storage.ProductImage] {
66
return images?.toArray() ?? []
77
}
88
var tagsArray: [Storage.ProductTag] {

Modules/Sources/Yosemite/Tools/ResultsController.swift

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import CoreData
99
public typealias ResultsControllerMutableType = NSManagedObject & ReadOnlyConvertible
1010

1111

12-
// MARK: - ResultsController
12+
// MARK: - GenericResultsController (Core Implementation)
1313
//
14-
public class ResultsController<T: ResultsControllerMutableType> {
14+
public class GenericResultsController<T: ResultsControllerMutableType, Output> {
1515

1616
/// The `StorageType` used to fetch objects.
1717
///
@@ -79,7 +79,7 @@ public class ResultsController<T: ResultsControllerMutableType> {
7979

8080
/// Closure to be executed whenever an Object is updated.
8181
///
82-
public var onDidChangeObject: ((_ object: T.ReadOnlyType, _ indexPath: IndexPath?, _ type: ChangeType, _ newIndexPath: IndexPath?) -> Void)?
82+
public var onDidChangeObject: ((_ object: Output, _ indexPath: IndexPath?, _ type: ChangeType, _ newIndexPath: IndexPath?) -> Void)?
8383

8484
/// Closure to be executed whenever an entire Section is updated.
8585
///
@@ -94,19 +94,25 @@ public class ResultsController<T: ResultsControllerMutableType> {
9494
///
9595
private let fetchLimit: Int?
9696

97+
/// Transformer closure to convert T to Output type.
98+
///
99+
private let transformer: (T) -> Output
100+
97101
/// Designated Initializer.
98102
///
99103
public init(viewStorage: StorageType,
100104
sectionNameKeyPath: String? = nil,
101105
matching predicate: NSPredicate? = nil,
102106
fetchLimit: Int? = nil,
103-
sortedBy descriptors: [NSSortDescriptor]) {
107+
sortedBy descriptors: [NSSortDescriptor],
108+
transformer: @escaping (T) -> Output) {
104109

105110
self.viewStorage = viewStorage
106111
self.sectionNameKeyPath = sectionNameKeyPath
107112
self.predicate = predicate
108113
self.fetchLimit = fetchLimit
109114
self.sortDescriptors = descriptors
115+
self.transformer = transformer
110116

111117
setupResultsController()
112118
setupEventsForwarding()
@@ -119,13 +125,15 @@ public class ResultsController<T: ResultsControllerMutableType> {
119125
sectionNameKeyPath: String? = nil,
120126
matching predicate: NSPredicate? = nil,
121127
fetchLimit: Int? = nil,
122-
sortedBy descriptors: [NSSortDescriptor]) {
128+
sortedBy descriptors: [NSSortDescriptor],
129+
transformer: @escaping (T) -> Output) {
123130

124131
self.init(viewStorage: storageManager.viewStorage,
125132
sectionNameKeyPath: sectionNameKeyPath,
126133
matching: predicate,
127134
fetchLimit: fetchLimit,
128-
sortedBy: descriptors)
135+
sortedBy: descriptors,
136+
transformer: transformer)
129137
}
130138

131139

@@ -139,14 +147,14 @@ public class ResultsController<T: ResultsControllerMutableType> {
139147
///
140148
/// Prefer to use `safeObject(at:)` instead.
141149
///
142-
public func object(at indexPath: IndexPath) -> T.ReadOnlyType {
143-
return controller.object(at: indexPath).toReadOnly()
150+
public func object(at indexPath: IndexPath) -> Output {
151+
return transformer(controller.object(at: indexPath))
144152
}
145153

146154
/// Returns the fetched object at the given `indexPath`. Returns `nil` if the `indexPath`
147155
/// does not exist.
148156
///
149-
public func safeObject(at indexPath: IndexPath) -> T.ReadOnlyType? {
157+
public func safeObject(at indexPath: IndexPath) -> Output? {
150158
guard !isEmpty else {
151159
return nil
152160
}
@@ -160,7 +168,7 @@ public class ResultsController<T: ResultsControllerMutableType> {
160168
return nil
161169
}
162170

163-
return controller.object(at: indexPath).toReadOnly()
171+
return transformer(controller.object(at: indexPath))
164172
}
165173

166174
/// Returns the Plain ObjectIndex corresponding to a given IndexPath. You can use this index to map the
@@ -194,23 +202,24 @@ public class ResultsController<T: ResultsControllerMutableType> {
194202
}
195203

196204
/// Returns an array of all of the (ReadOnly) Fetched Objects.
205+
/// Note: Avoid calling this in computed variables as the conversion of storage items can be costly.
197206
///
198-
public var fetchedObjects: [T.ReadOnlyType] {
199-
let readOnlyObjects = controller.fetchedObjects?.compactMap { mutableObject in
200-
mutableObject.toReadOnly()
207+
public var fetchedObjects: [Output] {
208+
let transformedObjects = controller.fetchedObjects?.compactMap { mutableObject in
209+
transformer(mutableObject)
201210
}
202211

203-
return readOnlyObjects ?? []
212+
return transformedObjects ?? []
204213
}
205214

206215
/// Returns an array of SectionInfo Entitites.
207216
///
208217
public var sections: [SectionInfo] {
209-
let readOnlySections = controller.sections?.compactMap { mutableSection in
210-
SectionInfo(mutableSection: mutableSection)
218+
let transformedSections = controller.sections?.compactMap { mutableSection in
219+
SectionInfo(mutableSection: mutableSection, transformer: transformer)
211220
}
212221

213-
return readOnlySections ?? []
222+
return transformedSections ?? []
214223
}
215224

216225
/// Returns an optional index path of the first matching object.
@@ -260,17 +269,17 @@ public class ResultsController<T: ResultsControllerMutableType> {
260269
return
261270
}
262271

263-
let readOnlyObject = object.toReadOnly()
264-
self.onDidChangeObject?(readOnlyObject, indexPath, type, newIndexPath)
272+
let transformedObject = transformer(object)
273+
self.onDidChangeObject?(transformedObject, indexPath, type, newIndexPath)
265274
}
266275

267276
internalDelegate.onDidChangeSection = { [weak self] (mutableSection, sectionIndex, type) in
268277
guard let `self` = self else {
269278
return
270279
}
271280

272-
let readOnlySection = SectionInfo(mutableSection: mutableSection)
273-
self.onDidChangeSection?(readOnlySection, sectionIndex, type)
281+
let transformedSection = SectionInfo(mutableSection: mutableSection, transformer: transformer)
282+
self.onDidChangeSection?(transformedSection, sectionIndex, type)
274283
}
275284
}
276285

@@ -294,7 +303,7 @@ public class ResultsController<T: ResultsControllerMutableType> {
294303

295304
// MARK: - Nested Types
296305
//
297-
public extension ResultsController {
306+
public extension GenericResultsController {
298307

299308
// MARK: - ResultsController.ChangeType
300309
//
@@ -322,9 +331,13 @@ public extension ResultsController {
322331
mutableSectionInfo.numberOfObjects
323332
}
324333

325-
/// Returns the array of (ReadOnly) objects in the section.
334+
/// Transformer closure to convert objects in the section.
335+
///
336+
private let transformer: (T) -> Output
337+
338+
/// Returns the array of transformed objects in the section.
326339
///
327-
private(set) public lazy var objects: [T.ReadOnlyType] = {
340+
private(set) public lazy var objects: [Output] = {
328341
guard let objects = mutableSectionInfo.objects else {
329342
return []
330343
}
@@ -333,13 +346,47 @@ public extension ResultsController {
333346
return []
334347
}
335348

336-
return castedObjects.map { $0.toReadOnly() }
349+
return castedObjects.map { transformer($0) }
337350
}()
338351

339352
/// Designated Initializer
340353
///
341-
init(mutableSection: NSFetchedResultsSectionInfo) {
354+
init(mutableSection: NSFetchedResultsSectionInfo, transformer: @escaping (T) -> Output) {
342355
mutableSectionInfo = mutableSection
356+
self.transformer = transformer
343357
}
344358
}
345359
}
360+
361+
// MARK: - ResultsController (Backward Compatible Specialization)
362+
//
363+
public class ResultsController<T: ResultsControllerMutableType>: GenericResultsController<T, T.ReadOnlyType> {
364+
/// Designated Initializer.
365+
///
366+
public init(viewStorage: StorageType,
367+
sectionNameKeyPath: String? = nil,
368+
matching predicate: NSPredicate? = nil,
369+
fetchLimit: Int? = nil,
370+
sortedBy descriptors: [NSSortDescriptor]) {
371+
super.init(viewStorage: viewStorage,
372+
sectionNameKeyPath: sectionNameKeyPath,
373+
matching: predicate,
374+
fetchLimit: fetchLimit,
375+
sortedBy: descriptors,
376+
transformer: { $0.toReadOnly() })
377+
}
378+
379+
/// Convenience Initializer.
380+
///
381+
public convenience init(storageManager: StorageManagerType,
382+
sectionNameKeyPath: String? = nil,
383+
matching predicate: NSPredicate? = nil,
384+
fetchLimit: Int? = nil,
385+
sortedBy descriptors: [NSSortDescriptor]) {
386+
self.init(viewStorage: storageManager.viewStorage,
387+
sectionNameKeyPath: sectionNameKeyPath,
388+
matching: predicate,
389+
fetchLimit: fetchLimit,
390+
sortedBy: descriptors)
391+
}
392+
}

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- [*] Increased decimal sensitivity in order creation to mitigate tax rounding issues [https://github.com/woocommerce/woocommerce-ios/pull/15957]
77
- [*] Fix initialization of authenticator to avoid crashes during login [https://github.com/woocommerce/woocommerce-ios/pull/15953]
88
- [*] Shipping Labels: Made HS tariff number field required in customs form for EU destinations [https://github.com/woocommerce/woocommerce-ios/pull/15946]
9+
- [*] Order Details: Attempt to improve performance by using a simplified version of product objects. [https://github.com/woocommerce/woocommerce-ios/pull/15959]
910
- [*] Order Details > Edit Shipping/Billing Address: Added map-based address lookup support for iOS 17+. [https://github.com/woocommerce/woocommerce-ios/pull/15964]
1011
- [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960]
1112
- [internal] Replace COTS_DEVICE reader model name with TAP_TO_PAY_DEVICE. [https://github.com/woocommerce/woocommerce-ios/pull/15961]

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,15 +601,15 @@ extension WooAnalyticsEvent {
601601
WooAnalyticsEvent(statName: .orderAddNew, properties: [:])
602602
}
603603

604-
static func orderProductsLoaded(order: Order, products: [Product], addOnGroups: [AddOnGroup]) -> WooAnalyticsEvent {
604+
static func orderProductsLoaded(order: Order, products: [OrderDetailsProduct], addOnGroups: [AddOnGroup]) -> WooAnalyticsEvent {
605605
let productTypes = productTypes(order: order, products: products)
606606
let hasAddOns = hasAddOns(order: order, products: products, addOnGroups: addOnGroups)
607607
return WooAnalyticsEvent(statName: .orderProductsLoaded, properties: [Keys.orderID: order.orderID,
608608
Keys.productTypes: productTypes,
609609
Keys.hasAddOns: hasAddOns])
610610
}
611611

612-
private static func hasAddOns(order: Order, products: [Product], addOnGroups: [AddOnGroup]) -> Bool {
612+
private static func hasAddOns(order: Order, products: [OrderDetailsProduct], addOnGroups: [AddOnGroup]) -> Bool {
613613
for item in order.items {
614614
guard let product = products.first(where: { $0.productID == item.productID }) else {
615615
continue
@@ -628,6 +628,13 @@ extension WooAnalyticsEvent {
628628
return false
629629
}
630630

631+
private static func productTypes(order: Order, products: [OrderDetailsProduct]) -> String {
632+
let productIDs = order.items.map { $0.productID }
633+
return productIDs.compactMap { productID in
634+
products.first(where: { $0.productID == productID })?.productType.rawValue
635+
}.uniqued().sorted().joined(separator: ",")
636+
}
637+
631638
private static func productTypes(order: Order, products: [Product]) -> String {
632639
let productIDs = order.items.map { $0.productID }
633640
return productIDs.compactMap { productID in

WooCommerce/Classes/ViewModels/Order Details/AddOns/AddOnCrossreferenceUseCase.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ struct AddOnCrossreferenceUseCase {
1515

1616
/// Product entity with known addOns that matches the order item.
1717
///
18-
private let product: Product
18+
private let product: OrderDetailsProduct
1919

2020
/// Global add-ons for the site.
2121
///
2222
private let addOnGroups: [AddOnGroup]
2323

24-
init(orderItemAttributes: [OrderItemAttribute], product: Product, addOnGroups: [AddOnGroup]) {
24+
init(orderItemAttributes: [OrderItemAttribute], product: OrderDetailsProduct, addOnGroups: [AddOnGroup]) {
2525
self.orderItemAttributes = orderItemAttributes
2626
self.product = product
2727
self.addOnGroups = addOnGroups

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsDataSource.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ final class OrderDetailsDataSource: NSObject {
149149

150150
/// Products from an Order
151151
///
152-
var products: [Product] = []
152+
var products: [OrderDetailsProduct] = []
153153

154154
/// Product variations from an order
155155
///
@@ -891,13 +891,14 @@ private extension OrderDetailsDataSource {
891891
}
892892

893893
let imageURL: URL? = {
894-
guard let imageURLString = aggregateItem.variationID != 0 ?
895-
lookUpProductVariation(productID: aggregateItem.productID, variationID: aggregateItem.variationID)?.image?.src:
896-
lookUpProduct(by: aggregateItem.productID)?.images.first?.src,
897-
let encodedImageURLString = imageURLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
898-
return nil
894+
if aggregateItem.variationID != 0 {
895+
guard let imageURLString = lookUpProductVariation(productID: aggregateItem.productID, variationID: aggregateItem.variationID)?.image?.src,
896+
let encodedImageURLString = imageURLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
897+
return nil
898+
}
899+
return URL(string: encodedImageURLString)
899900
}
900-
return URL(string: encodedImageURLString)
901+
return lookUpProduct(by: aggregateItem.productID)?.imageURL
901902
}()
902903

903904
let addOns: [OrderItemProductAddOn] = {
@@ -1234,7 +1235,7 @@ extension OrderDetailsDataSource {
12341235
return currentSiteStatuses.filter({$0.status == order.status}).first
12351236
}
12361237

1237-
func lookUpProduct(by productID: Int64) -> Product? {
1238+
func lookUpProduct(by productID: Int64) -> OrderDetailsProduct? {
12381239
return products.filter({ $0.productID == productID }).first
12391240
}
12401241

0 commit comments

Comments
 (0)