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:
@@ -44,12 +44,13 @@ struct MonthView: View {
|
||||
|
||||
/// Cached sorted year/month data to avoid recalculating in ForEach
|
||||
@State private var cachedSortedData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] = []
|
||||
@State private var cachedDemoSortedData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] = []
|
||||
|
||||
// MARK: - Demo Animation
|
||||
@StateObject private var demoManager = DemoAnimationManager.shared
|
||||
|
||||
/// Generate fake demo data for the past 12 months
|
||||
private var demoSortedData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] {
|
||||
private func computeDemoSortedData() -> [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] {
|
||||
var result: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] = []
|
||||
let calendar = Calendar.current
|
||||
let now = Date()
|
||||
@@ -158,7 +159,7 @@ struct MonthView: View {
|
||||
|
||||
/// Data to display - uses demo data when in demo mode, otherwise cached real data
|
||||
private var displayData: [(year: Int, months: [(month: Int, entries: [MoodEntryModel])])] {
|
||||
demoManager.isDemoMode ? demoSortedData : cachedSortedData
|
||||
demoManager.isDemoMode ? cachedDemoSortedData : cachedSortedData
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -362,7 +363,7 @@ struct MonthView: View {
|
||||
}
|
||||
.onAppear {
|
||||
cachedSortedData = computeSortedYearMonthData()
|
||||
// Demo mode is toggled manually via triple-tap
|
||||
cachedDemoSortedData = computeDemoSortedData()
|
||||
}
|
||||
.onChange(of: viewModel.numberOfItems) { _, _ in
|
||||
// Use numberOfItems as a lightweight proxy for data changes
|
||||
@@ -588,6 +589,8 @@ struct MonthCard: View, Equatable {
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("\(Random.monthName(fromMonthInt: month)) \(String(year)), \(showStats ? "expanded" : "collapsed")")
|
||||
.accessibilityHint("Double tap to toggle statistics")
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -599,6 +602,7 @@ struct MonthCard: View, Equatable {
|
||||
.foregroundColor(labelColor.opacity(0.6))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Share \(Random.monthName(fromMonthInt: month)) \(String(year)) mood data")
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
@@ -753,6 +757,7 @@ struct DemoHeatmapCell: View {
|
||||
// Generate random mood once when cell appears
|
||||
randomMood = DemoAnimationManager.randomPositiveMood()
|
||||
}
|
||||
.accessibilityLabel(accessibilityDescription)
|
||||
}
|
||||
|
||||
/// Whether this cell has been animated (filled with color)
|
||||
@@ -784,6 +789,18 @@ struct DemoHeatmapCell: View {
|
||||
return moodTint.color(forMood: entry.mood)
|
||||
}
|
||||
}
|
||||
|
||||
private var accessibilityDescription: String {
|
||||
if entry.mood == .placeholder {
|
||||
return "Empty day"
|
||||
} else if entry.mood == .missing {
|
||||
return "No mood logged for \(DateFormattingCache.shared.string(for: entry.forDate, format: .dateMedium))"
|
||||
} else if !isFiltered {
|
||||
return "\(DateFormattingCache.shared.string(for: entry.forDate, format: .dateMedium)): \(entry.mood.strValue) (filtered out)"
|
||||
} else {
|
||||
return "\(DateFormattingCache.shared.string(for: entry.forDate, format: .dateMedium)): \(entry.mood.strValue)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Mini Bar Chart
|
||||
|
||||
Reference in New Issue
Block a user