Fix 25 audit issues: memory leaks, concurrency, performance, accessibility

Address findings from comprehensive audit across 5 workstreams:

- Memory: Token-based DataController listeners (prevent closure leaks),
  static DateFormatters, ImageCache observer cleanup, MotionManager
  reference counting, FoundationModels dedup guard
- Concurrency: Replace Task.detached with Task in FeelsApp (preserve
  MainActor isolation), wrap WatchConnectivity handler in MainActor
- Performance: Cache sortedGroupedData in DayViewViewModel, cache demo
  data in MonthView/YearView, remove broken ReduceMotionModifier
- Accessibility: VoiceOver support for LockScreen, DemoHeatmapCell
  labels, MonthCard button labels, InsightsView header traits,
  Smart Invert protection on neon headers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-19 09:11:48 -06:00
parent b58dfd5093
commit c22d246865
18 changed files with 175 additions and 73 deletions

View File

@@ -95,11 +95,9 @@ struct DayView: View {
}
}
/// Cached sorted year/month data to avoid sorting dictionaries in ForEach
/// Sorted year/month data cached in ViewModel avoids re-sorting on every render
private var sortedGroupedData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] {
viewModel.grouped
.sorted { $0.key > $1.key }
.map { (year: $0.key, months: $0.value.sorted { $0.key > $1.key }.map { (month: $0.key, entries: $0.value) }) }
viewModel.sortedGroupedData
}
private var listView: some View {
@@ -313,6 +311,7 @@ extension DayView {
}
}
)
.accessibilityIgnoresInvertColors(true)
}
private func inkSectionHeader(month: Int, year: Int) -> some View {

View File

@@ -12,6 +12,7 @@ import SwiftData
class DayViewViewModel: ObservableObject {
@Published var grouped = [Int: [Int: [MoodEntryModel]]]()
@Published var numberOfItems = 0
@Published var sortedGroupedData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] = []
let addMonthStartWeekdayPadding: Bool
@@ -28,10 +29,12 @@ class DayViewViewModel: ObservableObject {
grouped.values.flatMap(\.values).reduce(0) { $0 + $1.count }
}
private var dataListenerToken: DataController.DataListenerToken?
init(addMonthStartWeekdayPadding: Bool) {
self.addMonthStartWeekdayPadding = addMonthStartWeekdayPadding
DataController.shared.addNewDataListener { [weak self] in
dataListenerToken = DataController.shared.addNewDataListener { [weak self] in
guard let self = self else { return }
// Avoid withAnimation for bulk data updates - it causes expensive view diffing
self.updateData()
@@ -39,6 +42,14 @@ class DayViewViewModel: ObservableObject {
updateData()
}
deinit {
if let token = dataListenerToken {
Task { @MainActor in
DataController.shared.removeDataListener(token: token)
}
}
}
private func getGroupedData(addMonthStartWeekdayPadding: Bool) {
var newStuff = DataController.shared.splitIntoYearMonth(includedDays: [1,2,3,4,5,6,7])
if addMonthStartWeekdayPadding {
@@ -50,6 +61,9 @@ class DayViewViewModel: ObservableObject {
public func updateData() {
getGroupedData(addMonthStartWeekdayPadding: self.addMonthStartWeekdayPadding)
sortedGroupedData = grouped
.sorted { $0.key > $1.key }
.map { (year: $0.key, months: $0.value.sorted { $0.key > $1.key }.map { (month: $0.key, entries: $0.value) }) }
}
public func add(mood: Mood, forDate date: Date, entryType: EntryType) {