Skip to content

Commit fe04802

Browse files
authored
Product List: Replace full product objects with simplified objects (#15966)
2 parents 7b8ec78 + 39a54d1 commit fe04802

25 files changed

+348
-157
lines changed

Modules/Sources/Yosemite/Tools/ResultsController.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public class GenericResultsController<T: ResultsControllerMutableType, Output> {
5151

5252
/// Internal NSFetchedResultsController Instance.
5353
///
54-
private lazy var controller: NSFetchedResultsController<T> = {
54+
public private(set) lazy var controller: NSFetchedResultsController<T> = {
5555
viewStorage.createFetchedResultsController(
5656
fetchRequest: fetchRequest,
5757
sectionNameKeyPath: sectionNameKeyPath,
@@ -65,10 +65,6 @@ public class GenericResultsController<T: ResultsControllerMutableType, Output> {
6565
// swiftlint:disable:next weak_delegate
6666
private let internalDelegate = FetchedResultsControllerDelegateWrapper()
6767

68-
/// NotificationCenter ObserverBlock Token
69-
///
70-
private var notificationCenterToken: Any?
71-
7268
/// Closure to be executed before the results are changed.
7369
///
7470
public var onWillChangeContent: (() -> Void)?

Modules/Tests/YosemiteTests/Tools/ResultsControllerTests.swift

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ final class ResultsControllerTests: XCTestCase {
5656
///
5757
func testResultsControllerPicksUpEntitiesAvailablePriorToInstantiation() {
5858
storageManager.insertSampleAccount()
59-
viewStorage.saveIfNeeded()
6059

6160
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage, sortedBy: [sampleSortDescriptor])
6261
try? resultsController.performFetch()
@@ -70,12 +69,11 @@ final class ResultsControllerTests: XCTestCase {
7069
/// Verifies that ResultsController does pick up entities inserted after being instantiated.
7170
///
7271
func testResultsControllerPicksUpEntitiesInsertedAfterInstantiation() {
72+
storageManager.insertSampleAccount()
73+
7374
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage, sortedBy: [sampleSortDescriptor])
7475
try? resultsController.performFetch()
7576

76-
storageManager.insertSampleAccount()
77-
viewStorage.saveIfNeeded()
78-
7977
XCTAssertEqual(resultsController.sections.count, 1)
8078
XCTAssertEqual(resultsController.sections.first?.objects.count, 1)
8179
XCTAssertEqual(resultsController.sections.first?.numberOfObjects, 1)
@@ -85,18 +83,16 @@ final class ResultsControllerTests: XCTestCase {
8583
/// Verifies that `sectionNameKeyPath` effectively causes the ResultsController to produce multiple sections, based on the grouping parameter.
8684
///
8785
func testResultsControllerGroupSectionsBySectionNameKeypath() {
88-
let sectionNameKeyPath = "userID"
89-
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage,
90-
sectionNameKeyPath: sectionNameKeyPath,
91-
sortedBy: [sampleSortDescriptor])
92-
try? resultsController.performFetch()
93-
9486
let numberOfAccounts = 100
9587
for _ in 0 ..< numberOfAccounts {
9688
storageManager.insertSampleAccount()
9789
}
9890

99-
viewStorage.saveIfNeeded()
91+
let sectionNameKeyPath = "userID"
92+
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage,
93+
sectionNameKeyPath: sectionNameKeyPath,
94+
sortedBy: [sampleSortDescriptor])
95+
try? resultsController.performFetch()
10096

10197
XCTAssertEqual(resultsController.sections.count, numberOfAccounts)
10298

@@ -109,15 +105,13 @@ final class ResultsControllerTests: XCTestCase {
109105
/// Verifies that `object(at indexPath:)` effectively returns the expected (ReadOnly) Entity.
110106
///
111107
func testObjectAtIndexPathReturnsExpectedEntity() {
108+
let mutableAccount = storageManager.insertSampleAccount()
112109
let sectionNameKeyPath = "userID"
113110
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage,
114111
sectionNameKeyPath: sectionNameKeyPath,
115112
sortedBy: [sampleSortDescriptor])
116113
try? resultsController.performFetch()
117114

118-
let mutableAccount = storageManager.insertSampleAccount()
119-
viewStorage.saveIfNeeded()
120-
121115
let indexPath = IndexPath(row: 0, section: 0)
122116
let readOnlyAccount = resultsController.object(at: indexPath)
123117

@@ -144,7 +138,6 @@ final class ResultsControllerTests: XCTestCase {
144138
}
145139

146140
storageManager.insertSampleAccount()
147-
viewStorage.saveIfNeeded()
148141

149142
waitForExpectations(timeout: Constants.expectationTimeout, handler: nil)
150143
}
@@ -167,7 +160,6 @@ final class ResultsControllerTests: XCTestCase {
167160
}
168161

169162
storageManager.insertSampleAccount()
170-
viewStorage.saveIfNeeded()
171163

172164
waitForExpectations(timeout: Constants.expectationTimeout, handler: nil)
173165
}
@@ -189,7 +181,6 @@ final class ResultsControllerTests: XCTestCase {
189181
}
190182

191183
storageManager.insertSampleAccount()
192-
viewStorage.saveIfNeeded()
193184

194185
waitForExpectations(timeout: Constants.expectationTimeout, handler: nil)
195186
}
@@ -211,7 +202,6 @@ final class ResultsControllerTests: XCTestCase {
211202
}
212203

213204
storageManager.insertSampleAccount()
214-
viewStorage.saveIfNeeded()
215205

216206
waitForExpectations(timeout: Constants.expectationTimeout, handler: nil)
217207
}
@@ -228,7 +218,6 @@ final class ResultsControllerTests: XCTestCase {
228218
let second = storageManager.insertSampleAccount().toReadOnly()
229219
let expected = [first.userID: first, second.userID: second]
230220

231-
viewStorage.saveIfNeeded()
232221

233222
for retrieved in resultsController.fetchedObjects {
234223
XCTAssertEqual(retrieved.username, expected[retrieved.userID]?.username)
@@ -239,14 +228,13 @@ final class ResultsControllerTests: XCTestCase {
239228
/// Verifies that `fetchedObjects` effectively returns all of the (readOnly) objects that are expected to be available.
240229
///
241230
func testResettingStorageIsMappedIntoOnResetClosure() {
231+
storageManager.insertSampleAccount()
232+
storageManager.insertSampleAccount()
233+
242234
let sortDescriptor = NSSortDescriptor(key: #selector(getter: StorageAccount.userID).description, ascending: true)
243235
let resultsController = ResultsController<StorageAccount>(viewStorage: viewStorage, sortedBy: [sortDescriptor])
244236
try? resultsController.performFetch()
245237

246-
storageManager.insertSampleAccount()
247-
storageManager.insertSampleAccount()
248-
249-
viewStorage.saveIfNeeded()
250238
XCTAssertEqual(resultsController.fetchedObjects.count, 2)
251239

252240
let expectation = self.expectation(description: "OnDidReset")
@@ -296,8 +284,6 @@ final class ResultsControllerTests: XCTestCase {
296284
}
297285
}
298286

299-
viewStorage.saveIfNeeded()
300-
301287
for (sectionNumber, sectionObject) in resultsController.sections.enumerated() {
302288
for (row, object) in sectionObject.objects.enumerated() {
303289
let indexPath = IndexPath(row: row, section: sectionNumber)

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
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]
99
- [*] Order Details: Attempt to improve performance by using a simplified version of product objects. [https://github.com/woocommerce/woocommerce-ios/pull/15959]
10+
- [*] Product List: Load list with simplified product objects to improve performance. [https://teamkiwip2.wordpress.com/2025/08/01/hack-week-improving-performance-when-loading-cached-products/]
1011
- [*] Order Details > Edit Shipping/Billing Address: Added map-based address lookup support for iOS 17+. [https://github.com/woocommerce/woocommerce-ios/pull/15964]
1112
- [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960]
1213
- [internal] Replace COTS_DEVICE reader model name with TAP_TO_PAY_DEVICE. [https://github.com/woocommerce/woocommerce-ios/pull/15961]

WooCommerce/Classes/Copiable/Models+Copiable.generated.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,54 @@ extension WooCommerce.AggregateOrderItem {
5050
}
5151
}
5252

53+
extension WooCommerce.ProductListItem {
54+
func copy(
55+
siteID: CopiableProp<Int64> = .copy,
56+
productID: CopiableProp<Int64> = .copy,
57+
name: CopiableProp<String> = .copy,
58+
productTypeKey: CopiableProp<String> = .copy,
59+
statusKey: CopiableProp<String> = .copy,
60+
sku: NullableCopiableProp<String> = .copy,
61+
manageStock: CopiableProp<Bool> = .copy,
62+
stockQuantity: NullableCopiableProp<Decimal> = .copy,
63+
stockStatusKey: CopiableProp<String> = .copy,
64+
imageURL: NullableCopiableProp<URL> = .copy,
65+
variations: CopiableProp<[Int64]> = .copy,
66+
bundleStockStatus: NullableCopiableProp<ProductStockStatus> = .copy,
67+
bundleStockQuantity: NullableCopiableProp<Int64> = .copy
68+
) -> WooCommerce.ProductListItem {
69+
let siteID = siteID ?? self.siteID
70+
let productID = productID ?? self.productID
71+
let name = name ?? self.name
72+
let productTypeKey = productTypeKey ?? self.productTypeKey
73+
let statusKey = statusKey ?? self.statusKey
74+
let sku = sku ?? self.sku
75+
let manageStock = manageStock ?? self.manageStock
76+
let stockQuantity = stockQuantity ?? self.stockQuantity
77+
let stockStatusKey = stockStatusKey ?? self.stockStatusKey
78+
let imageURL = imageURL ?? self.imageURL
79+
let variations = variations ?? self.variations
80+
let bundleStockStatus = bundleStockStatus ?? self.bundleStockStatus
81+
let bundleStockQuantity = bundleStockQuantity ?? self.bundleStockQuantity
82+
83+
return WooCommerce.ProductListItem(
84+
siteID: siteID,
85+
productID: productID,
86+
name: name,
87+
productTypeKey: productTypeKey,
88+
statusKey: statusKey,
89+
sku: sku,
90+
manageStock: manageStock,
91+
stockQuantity: stockQuantity,
92+
stockStatusKey: stockStatusKey,
93+
imageURL: imageURL,
94+
variations: variations,
95+
bundleStockStatus: bundleStockStatus,
96+
bundleStockQuantity: bundleStockQuantity
97+
)
98+
}
99+
}
100+
53101
extension WooCommerce.ShippingLabelSelectedRate {
54102
func copy(
55103
packageID: CopiableProp<String> = .copy,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Foundation
2+
import Yosemite
3+
4+
extension Product {
5+
func toListItem() -> ProductListItem {
6+
ProductListItem(siteID: siteID,
7+
productID: productID,
8+
name: name,
9+
productTypeKey: productTypeKey,
10+
statusKey: statusKey,
11+
sku: sku,
12+
manageStock: manageStock,
13+
stockQuantity: stockQuantity,
14+
stockStatusKey: stockStatusKey,
15+
imageURL: imageURL,
16+
variations: variations,
17+
bundleStockStatus: bundleStockStatus,
18+
bundleStockQuantity: bundleStockQuantity)
19+
}
20+
}

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

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,45 @@ import Yosemite
44
/// Represents a Product Entity with basic details to display in the product section of order details screen.
55
///
66
struct OrderDetailsProduct: Equatable {
7-
let siteID: Int64
87
let productID: Int64
9-
let name: String
10-
118
let productTypeKey: String
129
let sku: String?
1310

1411
let price: String
1512
let virtual: Bool
1613

17-
let stockQuantity: Decimal? // Core API reports Int or null; some extensions allow decimal values as well
18-
1914
let imageURL: URL?
2015

21-
let addOns: [Yosemite.ProductAddOn] //TODO: migrate AddOns to MetaData
16+
let addOns: [Yosemite.ProductAddOn]
2217

2318
var productType: ProductType {
2419
return ProductType(rawValue: productTypeKey)
2520
}
2621

27-
init(siteID: Int64,
28-
productID: Int64,
29-
name: String,
22+
/// periphery: ignore - used in test module
23+
init(productID: Int64,
3024
productTypeKey: String,
3125
sku: String?,
3226
price: String,
3327
virtual: Bool,
34-
stockQuantity: Decimal?,
3528
imageURL: URL?,
3629
addOns: [Yosemite.ProductAddOn]) {
37-
self.siteID = siteID
3830
self.productID = productID
39-
self.name = name
4031
self.productTypeKey = productTypeKey
4132
self.sku = sku
4233
self.price = price
4334
self.virtual = virtual
44-
self.stockQuantity = stockQuantity
4535
self.imageURL = imageURL
4636
self.addOns = addOns
4737
}
4838

4939
init(storageProduct: StorageProduct) {
50-
self.siteID = storageProduct.siteID
5140
self.productID = storageProduct.productID
52-
self.name = storageProduct.name
5341
self.productTypeKey = storageProduct.productTypeKey
5442
self.sku = storageProduct.sku
5543
self.price = storageProduct.price
5644
self.virtual = storageProduct.virtual
5745

58-
self.stockQuantity = {
59-
var quantity: Decimal?
60-
if let stockQuantity = storageProduct.stockQuantity {
61-
quantity = Decimal(string: stockQuantity)
62-
}
63-
return quantity
64-
}()
65-
6646
self.imageURL = storageProduct.imagesArray.first?.toReadOnly().imageURL
6747

6848
let addOnsArray: [StorageProductAddOn] = storageProduct.addOns?.toArray() ?? []

WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsViewModel.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ final class RefundDetailsViewModel {
2525
self.refund = refund
2626
}
2727

28-
/// Products from a Refund
29-
///
30-
var products: [OrderDetailsProduct] {
31-
return dataSource.products
32-
}
33-
3428
/// Subtotal from all refunded products
3529
///
3630
var itemSubtotal: String {

WooCommerce/Classes/ViewModels/ProductDetailsCellViewModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ struct ProductDetailsCellViewModel {
9292
self.isChildProduct = isChildProduct
9393
}
9494

95+
/// periphery: ignore - used in test module
9596
/// Order Item initializer
9697
///
9798
init(item: OrderItem,

WooCommerce/Classes/ViewRelated/Products/Cells/ProductsTabProductTableViewCell.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,16 @@ private struct ProductsTabProductTableViewCellRepresentable: UIViewRepresentable
305305
}
306306

307307
struct ProductsTabProductTableViewCell_Previews: PreviewProvider {
308-
private static var nonSelectedViewModel = ProductsTabProductViewModel(product: Product.swiftUIPreviewSample(), isSelected: false)
309-
private static var selectedViewModel = ProductsTabProductViewModel(product: Product.swiftUIPreviewSample().copy(statusKey: ProductStatus.pending.rawValue),
310-
isSelected: true)
308+
private static var nonSelectedViewModel = ProductsTabProductViewModel(
309+
product: Product.swiftUIPreviewSample().toListItem(),
310+
isSelected: false
311+
)
312+
private static var selectedViewModel = ProductsTabProductViewModel(
313+
product: Product.swiftUIPreviewSample()
314+
.toListItem()
315+
.copy(statusKey: ProductStatus.pending.rawValue),
316+
isSelected: true
317+
)
311318

312319
private static func makeStack() -> some View {
313320
VStack {

WooCommerce/Classes/ViewRelated/Products/Edit Product/EditableProductModel.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,14 +164,6 @@ extension EditableProductModel: ProductFormDataModel, TaxClassRequestable {
164164
product.bundledItems
165165
}
166166

167-
var bundleStockStatus: ProductStockStatus? {
168-
product.bundleStockStatus
169-
}
170-
171-
var bundleStockQuantity: Int64? {
172-
product.bundleStockQuantity
173-
}
174-
175167
var password: String? {
176168
product.password
177169
}

0 commit comments

Comments
 (0)