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:
@@ -16,7 +16,7 @@ final class ProgressViewModel {
|
||||
// MARK: - State
|
||||
|
||||
var selectedSport: Sport = .mlb
|
||||
var isLoading = false
|
||||
var isLoading = true
|
||||
var error: Error?
|
||||
var errorMessage: String?
|
||||
|
||||
@@ -42,10 +42,8 @@ final class ProgressViewModel {
|
||||
visits
|
||||
.filter { $0.sportEnum == selectedSport }
|
||||
.compactMap { visit -> String? in
|
||||
// Match visit's canonical stadium ID to a stadium
|
||||
stadiums.first { stadium in
|
||||
stadium.id == visit.stadiumId
|
||||
}?.id
|
||||
// O(1) dictionary lookup via DataProvider
|
||||
dataProvider.stadium(for: visit.stadiumId)?.id
|
||||
}
|
||||
)
|
||||
|
||||
@@ -120,7 +118,7 @@ final class ProgressViewModel {
|
||||
.sorted { $0.visitDate > $1.visitDate }
|
||||
.prefix(10)
|
||||
.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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,46 +20,19 @@ struct ProgressTabView: View {
|
||||
|
||||
@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 {
|
||||
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)
|
||||
}
|
||||
Group {
|
||||
if viewModel.stadiums.isEmpty {
|
||||
ProgressView("Loading...")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
scrollContent
|
||||
}
|
||||
.padding(Theme.Spacing.md)
|
||||
}
|
||||
.themedBackground()
|
||||
.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
|
||||
|
||||
private var leagueSelector: some View {
|
||||
@@ -385,7 +403,7 @@ struct ProgressTabView: View {
|
||||
}
|
||||
|
||||
ForEach(viewModel.recentVisits) { visitSummary in
|
||||
if let stadiumVisit = visits.first(where: { $0.id == visitSummary.id }) {
|
||||
if let stadiumVisit = visitsById[visitSummary.id] {
|
||||
NavigationLink {
|
||||
VisitDetailView(visit: stadiumVisit, stadium: visitSummary.stadium)
|
||||
} label: {
|
||||
|
||||
Reference in New Issue
Block a user