fix: 14 audit fixes — concurrency, memory, performance, accessibility
Second audit round addressing data races, task stacking, unbounded caches, and VoiceOver gaps across 7 files. Concurrency: - Move NSItemProvider @State access into MainActor block (3 drop handlers) - Cancel stale ScheduleViewModel tasks on rapid filter changes Memory: - Replace unbounded image dict with LRUCache(countLimit: 50) - Replace demo-mode asyncAfter with cancellable Task Performance: - Wrap debug NBA print() in #if DEBUG - Cache visitsById via @State + onChange instead of per-render computed - Pre-compute sportProgressFractions in ProgressViewModel - Replace allGames computed property with hasGames bool check - Cache sortedTrips via @State + onChange in SavedTripsListView Accessibility: - Add combined VoiceOver label to progress ring - Combine away/home team text into single readable phrase - Hide decorative StadiumDetailSheet icon from VoiceOver - Add explicit accessibilityLabel to SportFilterChip - Add combined accessibilityLabel to GameRowView Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,7 @@ final class ScheduleViewModel {
|
||||
private(set) var errorMessage: String?
|
||||
|
||||
private let dataProvider = AppDataProvider.shared
|
||||
private var loadTask: Task<Void, Never>?
|
||||
|
||||
// MARK: - Pre-computed Groupings (avoid computed property overhead)
|
||||
|
||||
@@ -117,6 +118,7 @@ final class ScheduleViewModel {
|
||||
logger.info("📅 \(sport.rawValue): \(count) games")
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
// Debug: Print all NBA games
|
||||
let nbaGames = games.filter { $0.game.sport == .nba }
|
||||
print("🏀 [DEBUG] All NBA games in schedule (\(nbaGames.count) total):")
|
||||
@@ -124,6 +126,7 @@ final class ScheduleViewModel {
|
||||
let dateStr = game.game.dateTime.gameDateTimeString(in: game.stadium.timeZone)
|
||||
print("🏀 \(dateStr): \(game.awayTeam.name) @ \(game.homeTeam.name) (\(game.game.id))")
|
||||
}
|
||||
#endif
|
||||
|
||||
} catch let cloudKitError as CloudKitError {
|
||||
self.error = cloudKitError
|
||||
@@ -151,9 +154,8 @@ final class ScheduleViewModel {
|
||||
selectedSports.insert(sport)
|
||||
}
|
||||
AnalyticsManager.shared.track(.scheduleFiltered(sport: sport.rawValue, dateRange: "\(startDate.formatted(.dateTime.month().day())) - \(endDate.formatted(.dateTime.month().day()))"))
|
||||
Task {
|
||||
await loadGames()
|
||||
}
|
||||
loadTask?.cancel()
|
||||
loadTask = Task { await loadGames() }
|
||||
}
|
||||
|
||||
func resetFilters() {
|
||||
@@ -161,17 +163,15 @@ final class ScheduleViewModel {
|
||||
searchText = ""
|
||||
startDate = Date()
|
||||
endDate = Calendar.current.date(byAdding: .day, value: 14, to: Date()) ?? Date()
|
||||
Task {
|
||||
await loadGames()
|
||||
}
|
||||
loadTask?.cancel()
|
||||
loadTask = Task { await loadGames() }
|
||||
}
|
||||
|
||||
func updateDateRange(start: Date, end: Date) {
|
||||
startDate = start
|
||||
endDate = end
|
||||
Task {
|
||||
await loadGames()
|
||||
}
|
||||
loadTask?.cancel()
|
||||
loadTask = Task { await loadGames() }
|
||||
}
|
||||
|
||||
// MARK: - Filtering & Grouping (pre-computed, not computed properties)
|
||||
|
||||
Reference in New Issue
Block a user