perf: add pagination to game selection view
Fixes lag and color inconsistency in trip creation game selection. - Add pagination with 50 games per page to TripCreationViewModel - Add displayedAvailableGames, loadInitialAvailableGames, loadMoreAvailableGames - Update GamePickerSheet to use paginated data with infinite scroll trigger - Show "Load more games" button with progress indicator and count Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,28 @@ final class TripCreationViewModel {
|
|||||||
var availableGames: [RichGame] = []
|
var availableGames: [RichGame] = []
|
||||||
var isLoadingGames: Bool = false
|
var isLoadingGames: Bool = false
|
||||||
|
|
||||||
|
// MARK: - Game Pagination
|
||||||
|
private let gamePageSize = 50
|
||||||
|
var displayedAvailableGames: [RichGame] = []
|
||||||
|
private var currentGamePage = 0
|
||||||
|
|
||||||
|
var hasMoreAvailableGames: Bool {
|
||||||
|
displayedAvailableGames.count < availableGames.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadInitialAvailableGames() {
|
||||||
|
currentGamePage = 0
|
||||||
|
displayedAvailableGames = Array(availableGames.prefix(gamePageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMoreAvailableGames() {
|
||||||
|
guard hasMoreAvailableGames else { return }
|
||||||
|
currentGamePage += 1
|
||||||
|
let start = currentGamePage * gamePageSize
|
||||||
|
let end = min(start + gamePageSize, availableGames.count)
|
||||||
|
displayedAvailableGames.append(contentsOf: availableGames[start..<end])
|
||||||
|
}
|
||||||
|
|
||||||
// Travel
|
// Travel
|
||||||
var travelMode: TravelMode = .drive
|
var travelMode: TravelMode = .drive
|
||||||
var routePreference: RoutePreference = .balanced
|
var routePreference: RoutePreference = .balanced
|
||||||
@@ -276,6 +298,9 @@ final class TripCreationViewModel {
|
|||||||
}
|
}
|
||||||
return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium)
|
return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize paginated display
|
||||||
|
loadInitialAvailableGames()
|
||||||
} catch {
|
} catch {
|
||||||
viewState = .error("Failed to load schedule data: \(error.localizedDescription)")
|
viewState = .error("Failed to load schedule data: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
@@ -501,6 +526,9 @@ final class TripCreationViewModel {
|
|||||||
return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium)
|
return RichGame(game: game, homeTeam: homeTeam, awayTeam: awayTeam, stadium: stadium)
|
||||||
}.sorted { $0.game.dateTime < $1.game.dateTime }
|
}.sorted { $0.game.dateTime < $1.game.dateTime }
|
||||||
|
|
||||||
|
// Initialize paginated display
|
||||||
|
loadInitialAvailableGames()
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
viewState = .error("Failed to load games: \(error.localizedDescription)")
|
viewState = .error("Failed to load games: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
@@ -541,6 +569,8 @@ final class TripCreationViewModel {
|
|||||||
mustStopLocations = []
|
mustStopLocations = []
|
||||||
preferredCities = []
|
preferredCities = []
|
||||||
availableGames = []
|
availableGames = []
|
||||||
|
displayedAvailableGames = []
|
||||||
|
currentGamePage = 0
|
||||||
isLoadingGames = false
|
isLoadingGames = false
|
||||||
currentPreferences = nil
|
currentPreferences = nil
|
||||||
allowRepeatCities = true
|
allowRepeatCities = true
|
||||||
|
|||||||
@@ -114,8 +114,11 @@ struct TripCreationView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $showGamePicker) {
|
.sheet(isPresented: $showGamePicker) {
|
||||||
GamePickerSheet(
|
GamePickerSheet(
|
||||||
games: viewModel.availableGames,
|
games: viewModel.displayedAvailableGames,
|
||||||
selectedIds: $viewModel.mustSeeGameIds
|
selectedIds: $viewModel.mustSeeGameIds,
|
||||||
|
hasMoreGames: viewModel.hasMoreAvailableGames,
|
||||||
|
totalGamesCount: viewModel.availableGames.count,
|
||||||
|
loadMoreGames: { viewModel.loadMoreAvailableGames() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showCityInput) {
|
.sheet(isPresented: $showCityInput) {
|
||||||
@@ -452,6 +455,12 @@ struct TripCreationView: View {
|
|||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(Theme.textSecondary(colorScheme))
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
// Ensure pagination is initialized when view appears
|
||||||
|
if viewModel.displayedAvailableGames.isEmpty && !viewModel.availableGames.isEmpty {
|
||||||
|
viewModel.loadInitialAvailableGames()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -950,6 +959,9 @@ extension TripCreationViewModel.ViewState {
|
|||||||
struct GamePickerSheet: View {
|
struct GamePickerSheet: View {
|
||||||
let games: [RichGame]
|
let games: [RichGame]
|
||||||
@Binding var selectedIds: Set<String>
|
@Binding var selectedIds: Set<String>
|
||||||
|
let hasMoreGames: Bool
|
||||||
|
let totalGamesCount: Int
|
||||||
|
let loadMoreGames: () -> Void
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
@@ -1047,6 +1059,28 @@ struct GamePickerSheet: View {
|
|||||||
selectedCount: selectedCountForSport(sport)
|
selectedCount: selectedCountForSport(sport)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load more indicator with infinite scroll trigger
|
||||||
|
if hasMoreGames {
|
||||||
|
Button {
|
||||||
|
loadMoreGames()
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: Theme.Spacing.sm) {
|
||||||
|
ProgressView()
|
||||||
|
.tint(Theme.warmOrange)
|
||||||
|
Text("Load more games (\(games.count) of \(totalGamesCount))")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(Theme.textSecondary(colorScheme))
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(Theme.Spacing.md)
|
||||||
|
.background(Theme.cardBackground(colorScheme))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.onAppear {
|
||||||
|
loadMoreGames()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.themedBackground()
|
.themedBackground()
|
||||||
|
|||||||
Reference in New Issue
Block a user