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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-04-13 - Batch @Published array mutations
**Learning:** In SwiftUI `ObservableObject` view models, mutating individual elements of a `@Published` array property inside a loop triggers a UI update notification for every change. This can cause severe performance issues when selecting/deselecting large lists.
**Action:** For collections of value types (structs), always utilize functional methods like `.map` to batch updates into a single property assignment, significantly reducing unnecessary UI recalculations. Always add comments explaining this optimization to prevent future developers from 'optimizing' it back to a standard `for` loop, as `.map` can appear slower in standard Swift contexts.
39 changes: 31 additions & 8 deletions Sources/Cacheout/ViewModels/CacheoutViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,26 @@ class CacheoutViewModel: ObservableObject {
}
}

// ⚑ Bolt: Batching array updates using .map instead of mutating elements in a loop.
// Mutating individual elements of a @Published array inside a loop triggers a UI
// update for every change. Using .map to assign a new array at once batches these
// updates into a single UI recalculation, improving performance on large lists.

func selectAllSafe() {
for i in scanResults.indices where scanResults[i].category.riskLevel == .safe && !scanResults[i].isEmpty {
scanResults[i].isSelected = true
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
scanResults = scanResults.map { result in
var updated = result
updated.isSelected = false
return updated
}
deselectAllNodeModules()
}
Expand All @@ -193,17 +204,29 @@ class CacheoutViewModel: ObservableObject {
}

func selectStaleNodeModules() {
for i in nodeModulesItems.indices where nodeModulesItems[i].isStale {
nodeModulesItems[i].isSelected = true
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 }
nodeModulesItems = nodeModulesItems.map { item in
var updated = item
updated.isSelected = true
return updated
}
}

func deselectAllNodeModules() {
for i in nodeModulesItems.indices { nodeModulesItems[i].isSelected = false }
nodeModulesItems = nodeModulesItems.map { item in
var updated = item
updated.isSelected = false
return updated
}
}

/// Menu bar label: show free GB in the tray
Expand Down