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:
Trey t
2026-02-18 22:30:30 -06:00
parent c32a08a49e
commit d0cbf75fc4
7 changed files with 75 additions and 48 deletions

View File

@@ -46,6 +46,9 @@ final class ProgressViewModel {
/// Unvisited stadiums for the selected sport
private(set) var unvisitedStadiums: [Stadium] = []
/// Pre-computed progress fractions per sport (avoids filtering visits per sport per render)
private(set) var sportProgressFractions: [Sport: Double] = [:]
/// Stadiums for the selected sport
var sportStadiums: [Stadium] {
stadiums.filter { $0.sport == selectedSport }
@@ -200,6 +203,19 @@ final class ProgressViewModel {
}
}
stadiumVisitStatus = statusMap
// Pre-compute sport progress fractions for SportSelectorGrid
var sportCounts: [Sport: Int] = [:]
for visit in visits {
if let sport = visit.sportEnum {
sportCounts[sport, default: 0] += 1
}
}
sportProgressFractions = Dictionary(uniqueKeysWithValues: Sport.supported.map { sport in
let total = LeagueStructure.stadiumCount(for: sport)
let visited = min(sportCounts[sport] ?? 0, total)
return (sport, total > 0 ? Double(visited) / Double(total) : 0)
})
}
// MARK: - Progress Card Generation