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

@@ -11,17 +11,17 @@ struct ScheduleListView: View {
@State private var showDatePicker = false
@State private var showDiagnostics = false
private var allGames: [RichGame] {
viewModel.gamesBySport.flatMap(\.games)
private var hasGames: Bool {
!viewModel.gamesBySport.isEmpty
}
var body: some View {
Group {
if viewModel.isLoading && allGames.isEmpty {
if viewModel.isLoading && !hasGames {
loadingView
} else if let errorMessage = viewModel.errorMessage {
errorView(message: errorMessage)
} else if allGames.isEmpty {
} else if !hasGames {
emptyView
} else {
gamesList
@@ -224,6 +224,7 @@ struct SportFilterChip: View {
}
.buttonStyle(.plain)
.accessibilityIdentifier("schedule.sport.\(sport.rawValue.lowercased())")
.accessibilityLabel(sport.rawValue)
.accessibilityValue(isSelected ? "Selected" : "Not selected")
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
@@ -313,6 +314,18 @@ struct GameRowView: View {
}
.padding(.vertical, 4)
.listRowBackground(isToday ? Color.orange.opacity(0.1) : nil)
.accessibilityElement(children: .ignore)
.accessibilityLabel(gameAccessibilityLabel)
}
private var gameAccessibilityLabel: String {
var parts = ["\(game.awayTeam.name) at \(game.homeTeam.name)"]
parts.append(game.stadium.name)
parts.append(game.localGameTimeShort)
if showDate {
parts.append(Self.dateFormatter.string(from: game.game.dateTime))
}
return parts.joined(separator: ", ")
}
}