perf: add pagination to schedule list
Loads 50 games at a time to fix lag with large datasets. - Add pagination state (displayedGames, currentPage, allFilteredGames) - Add loadInitialGames() and loadMoreGames() methods - Update gamesBySport to use displayedGames - Add infinite scroll trigger via onAppear on last game - Add ProgressView indicator when more games available - Reset pagination when search text changes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,22 @@ final class ScheduleViewModel {
|
|||||||
|
|
||||||
private let dataProvider = AppDataProvider.shared
|
private let dataProvider = AppDataProvider.shared
|
||||||
|
|
||||||
|
// MARK: - Pagination
|
||||||
|
|
||||||
|
private let pageSize = 50
|
||||||
|
private(set) var displayedGames: [RichGame] = []
|
||||||
|
private var currentPage = 0
|
||||||
|
private var allFilteredGames: [RichGame] = []
|
||||||
|
|
||||||
|
var hasMoreGames: Bool {
|
||||||
|
displayedGames.count < allFilteredGames.count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last game in the displayed list, used for infinite scroll detection
|
||||||
|
var lastDisplayedGame: RichGame? {
|
||||||
|
displayedGames.last
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
var filteredGames: [RichGame] {
|
var filteredGames: [RichGame] {
|
||||||
@@ -51,7 +67,7 @@ final class ScheduleViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var gamesBySport: [(sport: Sport, games: [RichGame])] {
|
var gamesBySport: [(sport: Sport, games: [RichGame])] {
|
||||||
let grouped = Dictionary(grouping: filteredGames) { game in
|
let grouped = Dictionary(grouping: displayedGames) { game in
|
||||||
game.game.sport
|
game.game.sport
|
||||||
}
|
}
|
||||||
// Sort by sport order (use allCases index for consistent ordering)
|
// Sort by sport order (use allCases index for consistent ordering)
|
||||||
@@ -69,11 +85,35 @@ final class ScheduleViewModel {
|
|||||||
selectedSports.count < Sport.supported.count || !searchText.isEmpty
|
selectedSports.count < Sport.supported.count || !searchText.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Pagination Actions
|
||||||
|
|
||||||
|
/// Updates filtered games list based on current search text and resets pagination
|
||||||
|
func updateFilteredGames() {
|
||||||
|
allFilteredGames = filteredGames
|
||||||
|
loadInitialGames()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the first page of games
|
||||||
|
func loadInitialGames() {
|
||||||
|
currentPage = 0
|
||||||
|
displayedGames = Array(allFilteredGames.prefix(pageSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads more games when scrolling to the bottom
|
||||||
|
func loadMoreGames() {
|
||||||
|
guard hasMoreGames else { return }
|
||||||
|
currentPage += 1
|
||||||
|
let start = currentPage * pageSize
|
||||||
|
let end = min(start + pageSize, allFilteredGames.count)
|
||||||
|
displayedGames.append(contentsOf: allFilteredGames[start..<end])
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
func loadGames() async {
|
func loadGames() async {
|
||||||
guard !selectedSports.isEmpty else {
|
guard !selectedSports.isEmpty else {
|
||||||
games = []
|
games = []
|
||||||
|
updateFilteredGames()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +132,7 @@ final class ScheduleViewModel {
|
|||||||
self.errorMessage = providerError
|
self.errorMessage = providerError
|
||||||
self.error = dataProvider.error
|
self.error = dataProvider.error
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
updateFilteredGames()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +150,7 @@ final class ScheduleViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
updateFilteredGames()
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearError() {
|
func clearError() {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ struct ScheduleListView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if viewModel.isLoading && viewModel.games.isEmpty {
|
if viewModel.isLoading && viewModel.displayedGames.isEmpty {
|
||||||
loadingView
|
loadingView
|
||||||
} else if let errorMessage = viewModel.errorMessage {
|
} else if let errorMessage = viewModel.errorMessage {
|
||||||
errorView(message: errorMessage)
|
errorView(message: errorMessage)
|
||||||
} else if viewModel.games.isEmpty {
|
} else if viewModel.displayedGames.isEmpty {
|
||||||
emptyView
|
emptyView
|
||||||
} else {
|
} else {
|
||||||
gamesList
|
gamesList
|
||||||
@@ -58,6 +58,9 @@ struct ScheduleListView: View {
|
|||||||
.task {
|
.task {
|
||||||
await viewModel.loadGames()
|
await viewModel.loadGames()
|
||||||
}
|
}
|
||||||
|
.onChange(of: viewModel.searchText) {
|
||||||
|
viewModel.updateFilteredGames()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Sport Filter
|
// MARK: - Sport Filter
|
||||||
@@ -93,6 +96,12 @@ struct ScheduleListView: View {
|
|||||||
Section {
|
Section {
|
||||||
ForEach(sportGroup.games) { richGame in
|
ForEach(sportGroup.games) { richGame in
|
||||||
GameRowView(game: richGame, showDate: true, showLocation: true)
|
GameRowView(game: richGame, showDate: true, showLocation: true)
|
||||||
|
.onAppear {
|
||||||
|
// Trigger load more when the last game appears
|
||||||
|
if richGame.id == viewModel.lastDisplayedGame?.id {
|
||||||
|
viewModel.loadMoreGames()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
@@ -103,6 +112,18 @@ struct ScheduleListView: View {
|
|||||||
}
|
}
|
||||||
.listRowBackground(Theme.cardBackground(colorScheme))
|
.listRowBackground(Theme.cardBackground(colorScheme))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show loading indicator when more games are available
|
||||||
|
if viewModel.hasMoreGames {
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
|
|||||||
Reference in New Issue
Block a user