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-05-15 - Batched `@Published` Array Updates
**Learning:** Mutating individual elements of a `@Published` array inside a `for` loop triggers a UI update notification for every single element change. For collections of structs, using `.map` to create a new array and assigning it back batches the UI update into a single event.
**Action:** Always use functional methods like `.map` instead of `for` loops when performing bulk modifications on `@Published` value-type collections in SwiftUI ViewModels, and add explanatory comments to prevent future regressions.
44 changes: 36 additions & 8 deletions Sources/Cacheout/ViewModels/CacheoutViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,24 @@ class CacheoutViewModel: ObservableObject {
}

func selectAllSafe() {
for i in scanResults.indices where scanResults[i].category.riskLevel == .safe && !scanResults[i].isEmpty {
scanResults[i].isSelected = true
// Optimization: Use .map to batch updates and trigger only ONE UI notification.
// Mutating elements inside a loop triggers a notification for EVERY element changed.
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
// Optimization: Use .map to batch updates and trigger only ONE UI notification.
// Mutating elements inside a loop triggers a notification for EVERY element changed.
scanResults = scanResults.map { result in
var updated = result
updated.isSelected = false
return updated
}
deselectAllNodeModules()
}
Expand All @@ -193,17 +203,35 @@ class CacheoutViewModel: ObservableObject {
}

func selectStaleNodeModules() {
for i in nodeModulesItems.indices where nodeModulesItems[i].isStale {
nodeModulesItems[i].isSelected = true
// Optimization: Use .map to batch updates and trigger only ONE UI notification.
// Mutating elements inside a loop triggers a notification for EVERY element changed.
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 }
// Optimization: Use .map to batch updates and trigger only ONE UI notification.
// Mutating elements inside a loop triggers a notification for EVERY element changed.
nodeModulesItems = nodeModulesItems.map { item in
var updated = item
updated.isSelected = true
return updated
}
}

func deselectAllNodeModules() {
for i in nodeModulesItems.indices { nodeModulesItems[i].isSelected = false }
// Optimization: Use .map to batch updates and trigger only ONE UI notification.
// Mutating elements inside a loop triggers a notification for EVERY element changed.
nodeModulesItems = nodeModulesItems.map { item in
var updated = item
updated.isSelected = false
return updated
}
}

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