Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 34 additions & 8 deletions Sources/Cacheout/ViewModels/CacheoutViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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
Expand Down