diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..80ed420 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,5 @@ +## 2024-04-11 - Optimize view model collection updates + +**Learning:** In SwiftUI `ObservableObject` view models, mutating individual elements of a `@Published` array property inside a `for` loop triggers a UI update notification for *every single change*. For collections of value types (structs), utilizing functional methods like `map` to batch updates into a single property assignment significantly reduces unnecessary UI recalculations and improves responsiveness. However, because `.map` creates a new array and looks like a performance degradation in standard Swift compared to in-place mutation, explicit comments are necessary to prevent future developers from "optimizing" it back to a `for` loop. + +**Action:** When updating multiple elements in a `@Published` collection in SwiftUI, use `.map` to batch the update into a single assignment. Always accompany this pattern with a `// PERFORMANCE:` comment explicitly explaining that this is a SwiftUI-specific optimization to prevent O(N) view re-renders. diff --git a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift index 13a9811..65c3027 100644 --- a/Sources/Cacheout/ViewModels/CacheoutViewModel.swift +++ b/Sources/Cacheout/ViewModels/CacheoutViewModel.swift @@ -172,14 +172,25 @@ class CacheoutViewModel: ObservableObject { } func selectAllSafe() { - for i in scanResults.indices where scanResults[i].category.riskLevel == .safe && !scanResults[i].isEmpty { - scanResults[i].isSelected = true + // PERFORMANCE: We use .map to batch updates into a single assignment. + // While a for loop would be faster in standard Swift, mutating individual + // elements of a @Published array inside a loop triggers O(N) UI view + // re-renders. This single assignment reduces it to O(1) updates. + scanResults = scanResults.map { result in + var updated = result + if updated.category.riskLevel == .safe && !updated.isEmpty { + updated.isSelected = true + } + return updated } } func deselectAll() { - for i in scanResults.indices { - scanResults[i].isSelected = false + // PERFORMANCE: Batch updates to @Published array to avoid O(N) UI re-renders. + scanResults = scanResults.map { result in + var updated = result + updated.isSelected = false + return updated } deselectAllNodeModules() } @@ -193,17 +204,32 @@ class CacheoutViewModel: ObservableObject { } func selectStaleNodeModules() { - for i in nodeModulesItems.indices where nodeModulesItems[i].isStale { - nodeModulesItems[i].isSelected = true + // PERFORMANCE: Batch updates to @Published array to avoid O(N) UI re-renders. + nodeModulesItems = nodeModulesItems.map { item in + var updated = item + if updated.isStale { + updated.isSelected = true + } + return updated } } func selectAllNodeModules() { - for i in nodeModulesItems.indices { nodeModulesItems[i].isSelected = true } + // PERFORMANCE: Batch updates to @Published array to avoid O(N) UI re-renders. + nodeModulesItems = nodeModulesItems.map { item in + var updated = item + updated.isSelected = true + return updated + } } func deselectAllNodeModules() { - for i in nodeModulesItems.indices { nodeModulesItems[i].isSelected = false } + // PERFORMANCE: Batch updates to @Published array to avoid O(N) UI re-renders. + nodeModulesItems = nodeModulesItems.map { item in + var updated = item + updated.isSelected = false + return updated + } } /// Menu bar label: show free GB in the tray