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:
@@ -26,6 +26,7 @@ struct YearView: View {
|
||||
|
||||
/// Cached sorted year keys to avoid re-sorting in ForEach on every render
|
||||
@State private var cachedSortedYearKeys: [Int] = []
|
||||
@State private var cachedDemoYearData: [Int: [Int: [DayChartView]]] = [:]
|
||||
|
||||
// MARK: - Demo Animation
|
||||
@StateObject private var demoManager = DemoAnimationManager.shared
|
||||
@@ -34,7 +35,7 @@ struct YearView: View {
|
||||
private let heatmapColumns = Array(repeating: GridItem(.flexible(), spacing: 2), count: 12)
|
||||
|
||||
/// Generate demo year data for the past 3 years (full 12 months each)
|
||||
private var demoYearData: [Int: [Int: [DayChartView]]] {
|
||||
private func computeDemoYearData() -> [Int: [Int: [DayChartView]]] {
|
||||
var result: [Int: [Int: [DayChartView]]] = [:]
|
||||
let calendar = Calendar.current
|
||||
let currentYear = calendar.component(.year, from: Date())
|
||||
@@ -107,7 +108,7 @@ struct YearView: View {
|
||||
/// Year keys to display - demo data or real data
|
||||
private var displayYearKeys: [Int] {
|
||||
if demoManager.isDemoMode {
|
||||
return Array(demoYearData.keys.sorted(by: >))
|
||||
return Array(cachedDemoYearData.keys.sorted(by: >))
|
||||
}
|
||||
return cachedSortedYearKeys
|
||||
}
|
||||
@@ -115,7 +116,7 @@ struct YearView: View {
|
||||
/// Year data for a specific year - demo or real
|
||||
private func yearDataFor(_ year: Int) -> [Int: [DayChartView]] {
|
||||
if demoManager.isDemoMode {
|
||||
return demoYearData[year] ?? [:]
|
||||
return cachedDemoYearData[year] ?? [:]
|
||||
}
|
||||
return viewModel.data[year] ?? [:]
|
||||
}
|
||||
@@ -282,7 +283,7 @@ struct YearView: View {
|
||||
.onAppear(perform: {
|
||||
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
|
||||
cachedSortedYearKeys = Array(viewModel.data.keys.sorted(by: >))
|
||||
// Demo mode is toggled manually via triple-tap
|
||||
cachedDemoYearData = computeDemoYearData()
|
||||
})
|
||||
.onChange(of: viewModel.data.keys.count) { _, _ in
|
||||
cachedSortedYearKeys = Array(viewModel.data.keys.sorted(by: >))
|
||||
@@ -787,6 +788,19 @@ struct DemoYearHeatmapCell: View {
|
||||
// Generate random mood once when cell appears
|
||||
randomMood = DemoAnimationManager.randomPositiveMood()
|
||||
}
|
||||
.accessibilityLabel(accessibilityDescription)
|
||||
}
|
||||
|
||||
private var accessibilityDescription: String {
|
||||
if !isFiltered {
|
||||
return "Filtered out"
|
||||
} else if color == Mood.placeholder.color {
|
||||
return "Empty"
|
||||
} else if color == Mood.missing.color {
|
||||
return "No mood logged"
|
||||
} else {
|
||||
return "Mood entry"
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this cell has been animated (filled with color)
|
||||
|
||||
@@ -24,13 +24,23 @@ class YearViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private var dataListenerToken: DataController.DataListenerToken?
|
||||
|
||||
init() {
|
||||
DataController.shared.addNewDataListener { [weak self] in
|
||||
dataListenerToken = DataController.shared.addNewDataListener { [weak self] in
|
||||
self?.refreshData()
|
||||
}
|
||||
updateData()
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let token = dataListenerToken {
|
||||
Task { @MainActor in
|
||||
DataController.shared.removeDataListener(token: token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-fetch data using the current date range. Called by the data listener
|
||||
/// when mood entries change in other tabs.
|
||||
public func refreshData() {
|
||||
|
||||
Reference in New Issue
Block a user