perf: optimize Progress tab with O(1) lookups and loading state

Replace O(n) linear searches with dictionary lookups in ProgressViewModel
and ProgressTabView. Add loading spinner while data loads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-09 17:38:59 -06:00
parent 213d2bae53
commit 1c57c47041
2 changed files with 61 additions and 45 deletions

View File

@@ -16,7 +16,7 @@ final class ProgressViewModel {
// MARK: - State // MARK: - State
var selectedSport: Sport = .mlb var selectedSport: Sport = .mlb
var isLoading = false var isLoading = true
var error: Error? var error: Error?
var errorMessage: String? var errorMessage: String?
@@ -42,10 +42,8 @@ final class ProgressViewModel {
visits visits
.filter { $0.sportEnum == selectedSport } .filter { $0.sportEnum == selectedSport }
.compactMap { visit -> String? in .compactMap { visit -> String? in
// Match visit's canonical stadium ID to a stadium // O(1) dictionary lookup via DataProvider
stadiums.first { stadium in dataProvider.stadium(for: visit.stadiumId)?.id
stadium.id == visit.stadiumId
}?.id
} }
) )
@@ -120,7 +118,7 @@ final class ProgressViewModel {
.sorted { $0.visitDate > $1.visitDate } .sorted { $0.visitDate > $1.visitDate }
.prefix(10) .prefix(10)
.compactMap { visit -> VisitSummary? in .compactMap { visit -> VisitSummary? in
guard let stadium = stadiums.first(where: { $0.id == visit.stadiumId }), guard let stadium = dataProvider.stadium(for: visit.stadiumId),
let sport = visit.sportEnum else { let sport = visit.sportEnum else {
return nil return nil
} }

View File

@@ -20,46 +20,19 @@ struct ProgressTabView: View {
@Query private var visits: [StadiumVisit] @Query private var visits: [StadiumVisit]
/// O(1) lookup for visits by ID (built from @Query results)
private var visitsById: [UUID: StadiumVisit] {
Dictionary(uniqueKeysWithValues: visits.map { ($0.id, $0) })
}
var body: some View { var body: some View {
ScrollView { Group {
VStack(spacing: Theme.Spacing.lg) { if viewModel.stadiums.isEmpty {
// League Selector ProgressView("Loading...")
leagueSelector .frame(maxWidth: .infinity, maxHeight: .infinity)
.staggeredAnimation(index: 0) } else {
scrollContent
// Progress Summary Card
progressSummaryCard
.staggeredAnimation(index: 1)
// Map View
ProgressMapView(
stadiums: viewModel.sportStadiums,
visitStatus: viewModel.stadiumVisitStatus,
selectedStadium: $selectedStadium
)
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.staggeredAnimation(index: 2)
// Stadium Lists
stadiumListsSection
.staggeredAnimation(index: 3)
// Achievements Teaser
achievementsSection
.staggeredAnimation(index: 4)
// Recent Visits
if !viewModel.recentVisits.isEmpty {
recentVisitsSection
.staggeredAnimation(index: 5)
}
} }
.padding(Theme.Spacing.md)
} }
.themedBackground() .themedBackground()
.toolbar { .toolbar {
@@ -126,6 +99,51 @@ struct ProgressTabView: View {
} }
} }
// MARK: - Scroll Content
private var scrollContent: some View {
ScrollView {
VStack(spacing: Theme.Spacing.lg) {
// League Selector
leagueSelector
.staggeredAnimation(index: 0)
// Progress Summary Card
progressSummaryCard
.staggeredAnimation(index: 1)
// Map View
ProgressMapView(
stadiums: viewModel.sportStadiums,
visitStatus: viewModel.stadiumVisitStatus,
selectedStadium: $selectedStadium
)
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: Theme.CornerRadius.large))
.overlay {
RoundedRectangle(cornerRadius: Theme.CornerRadius.large)
.stroke(Theme.surfaceGlow(colorScheme), lineWidth: 1)
}
.staggeredAnimation(index: 2)
// Stadium Lists
stadiumListsSection
.staggeredAnimation(index: 3)
// Achievements Teaser
achievementsSection
.staggeredAnimation(index: 4)
// Recent Visits
if !viewModel.recentVisits.isEmpty {
recentVisitsSection
.staggeredAnimation(index: 5)
}
}
.padding(Theme.Spacing.md)
}
}
// MARK: - League Selector // MARK: - League Selector
private var leagueSelector: some View { private var leagueSelector: some View {
@@ -385,7 +403,7 @@ struct ProgressTabView: View {
} }
ForEach(viewModel.recentVisits) { visitSummary in ForEach(viewModel.recentVisits) { visitSummary in
if let stadiumVisit = visits.first(where: { $0.id == visitSummary.id }) { if let stadiumVisit = visitsById[visitSummary.id] {
NavigationLink { NavigationLink {
VisitDetailView(visit: stadiumVisit, stadium: visitSummary.stadium) VisitDetailView(visit: stadiumVisit, stadium: visitSummary.stadium)
} label: { } label: {