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:
@@ -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