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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user