fix: 22 audit fixes — concurrency, memory, performance, accessibility

- Move 7 Data(contentsOf:) calls off MainActor via Task.detached (BootstrapService)
- Batch-fetch N+1 queries in sync merge loops (CanonicalSyncService)
- Predicate-based gamesForTeam fetch instead of fetching all games (DataProvider)
- Proper Sendable on RouteInfo with nonisolated(unsafe) polyline (LocationService)
- [weak self] in BGTaskScheduler register closures (BackgroundSyncManager)
- Cache tripDays, routeWaypoints as @State with recompute (TripDetailView)
- Remove unused AnyCancellable, add Task lifecycle management (TripDetailView)
- Cache sportStadiums, recentVisits as stored properties (ProgressViewModel)
- Dynamic Type fonts replacing hardcoded sizes (OnboardingPaywallView)
- Accessibility labels/hints on stadium picker, date picker, map, stats,
  settings toggle, and day cards

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-19 09:23:29 -06:00
parent dad3270be7
commit e7420061a5
12 changed files with 180 additions and 101 deletions

View File

@@ -49,10 +49,8 @@ final class ProgressViewModel {
/// 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 }
}
/// Stadiums for the selected sport (cached, recomputed on data change)
private(set) var sportStadiums: [Stadium] = []
/// Count of trips for the selected sport (stub - can be enhanced)
var tripCount: Int {
@@ -60,30 +58,8 @@ final class ProgressViewModel {
0
}
/// Recent visits sorted by date
var recentVisits: [VisitSummary] {
visits
.sorted { $0.visitDate > $1.visitDate }
.prefix(10)
.compactMap { visit -> VisitSummary? in
guard let stadium = dataProvider.stadium(for: visit.stadiumId),
let sport = visit.sportEnum else {
return nil
}
return VisitSummary(
id: visit.id,
stadium: stadium,
visitDate: visit.visitDate,
visitType: visit.visitType,
sport: sport,
homeTeamName: visit.homeTeamName,
awayTeamName: visit.awayTeamName,
score: visit.finalScore,
photoCount: visit.photoMetadata?.count ?? 0,
notes: visit.notes
)
}
}
/// Recent visits sorted by date (cached, recomputed on data change)
private(set) var recentVisits: [VisitSummary] = []
// MARK: - Configuration
@@ -154,7 +130,8 @@ final class ProgressViewModel {
private func recomputeDerivedState() {
// Compute league progress once
let sportStadiums = stadiums.filter { $0.sport == selectedSport }
let filteredStadiums = stadiums.filter { $0.sport == selectedSport }
self.sportStadiums = filteredStadiums
let visitedStadiumIds = Set(
visits
@@ -164,12 +141,12 @@ final class ProgressViewModel {
}
)
let visited = sportStadiums.filter { visitedStadiumIds.contains($0.id) }
let remaining = sportStadiums.filter { !visitedStadiumIds.contains($0.id) }
let visited = filteredStadiums.filter { visitedStadiumIds.contains($0.id) }
let remaining = filteredStadiums.filter { !visitedStadiumIds.contains($0.id) }
leagueProgress = LeagueProgress(
sport: selectedSport,
totalStadiums: sportStadiums.count,
totalStadiums: filteredStadiums.count,
visitedStadiums: visited.count,
stadiumsVisited: visited,
stadiumsRemaining: remaining
@@ -216,6 +193,33 @@ final class ProgressViewModel {
let visited = min(sportCounts[sport] ?? 0, total)
return (sport, total > 0 ? Double(visited) / Double(total) : 0)
})
// Recompute recent visits cache
recomputeRecentVisits()
}
private func recomputeRecentVisits() {
recentVisits = visits
.sorted { $0.visitDate > $1.visitDate }
.prefix(10)
.compactMap { visit -> VisitSummary? in
guard let stadium = dataProvider.stadium(for: visit.stadiumId),
let sport = visit.sportEnum else {
return nil
}
return VisitSummary(
id: visit.id,
stadium: stadium,
visitDate: visit.visitDate,
visitType: visit.visitType,
sport: sport,
homeTeamName: visit.homeTeamName,
awayTeamName: visit.awayTeamName,
score: visit.finalScore,
photoCount: visit.photoMetadata?.count ?? 0,
notes: visit.notes
)
}
}
// MARK: - Progress Card Generation