Skip to content

Commit 4c4bf4d

Browse files
Hoang Phamclaude
andcommitted
Update Pokemon demo to use infinite query system
- Add fetchPokemonPage API method for pagination support - Replace PokemonListView with UseInfiniteQuery implementation - Add infinite scrolling with automatic page loading - Include manual "Load More" button and loading states - Add end-of-list indicator when all Pokemon are loaded - Reduce API delay for better user experience - Update description to reflect infinite scrolling functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7ff6dc6 commit 4c4bf4d

2 files changed

Lines changed: 100 additions & 12 deletions

File tree

Example/swiftui-query-demo/ContentView.swift

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ struct ContentView: View {
3737
DemoButton(
3838
icon: "list.bullet.rectangle",
3939
title: "Pokemon List",
40-
description: "Browse Pokemon with caching and infinite loading"
40+
description: "Infinite scrolling Pokemon list with automatic pagination"
4141
)
4242
}
4343

@@ -118,29 +118,108 @@ struct PokemonListView: View {
118118

119119
var body: some View {
120120
WithPerceptionTracking {
121-
UseQuery(
122-
queryKey: "pokemon-list",
123-
queryFn: { _ in try await PokemonAPI.fetchPokemonList(limit: 50) },
124-
staleTime: 5 // 5 minutes before considered stale
121+
UseInfiniteQuery(
122+
queryKey: "pokemon-infinite-list",
123+
queryFn: { _, pageParam in
124+
try await PokemonAPI.fetchPokemonPage(offset: pageParam ?? 0)
125+
},
126+
getNextPageParam: { pages in
127+
// Calculate next offset based on current pages
128+
let currentTotal = pages.reduce(0) { total, page in total + page.results.count }
129+
let lastPage = pages.last
130+
131+
// If we have next URL or haven't reached the total count, continue pagination
132+
if let lastPage, lastPage.next != nil {
133+
return currentTotal
134+
}
135+
return nil // No more pages
136+
},
137+
initialPageParam: 0,
138+
staleTime: 5 * 60 // 5 minutes before considered stale
125139
) { result in
126-
if result.isLoading {
140+
if result.isLoading, result.data?.pages.isEmpty != false {
141+
// Initial loading state
127142
VStack(spacing: 16) {
128143
ProgressView()
129144
.scaleEffect(1.2)
130145
Text("Loading Pokemon...")
131146
.foregroundColor(.secondary)
132147
}
133148
.frame(maxWidth: .infinity, maxHeight: .infinity)
134-
} else if let error = result.error {
149+
} else if let error = result.error, result.data?.pages.isEmpty != false {
150+
// Error state when no data is loaded
135151
ErrorView(error: error) {
136152
Task {
137153
_ = try? await result.refetch()
138154
}
139155
}
140-
} else if let pokemonList = result.data {
141-
List(pokemonList.results) { pokemon in
142-
NavigationLink(destination: PokemonDetailView(pokemonId: pokemon.pokemonId)) {
143-
PokemonListRow(pokemon: pokemon)
156+
} else if let infiniteData = result.data {
157+
// Show the list with infinite scrolling
158+
ScrollView {
159+
LazyVStack(spacing: 0) {
160+
// Render all Pokemon from all pages
161+
ForEach(infiniteData.pages.indices, id: \.self) { pageIndex in
162+
let page = infiniteData.pages[pageIndex]
163+
ForEach(page.results) { pokemon in
164+
NavigationLink(destination: PokemonDetailView(pokemonId: pokemon.pokemonId)) {
165+
PokemonListRow(pokemon: pokemon)
166+
.padding(.horizontal)
167+
.padding(.vertical, 8)
168+
}
169+
.buttonStyle(PlainButtonStyle())
170+
171+
// Add divider except for last item
172+
if pokemon.id != page.results.last?.id || pageIndex != infiniteData.pages
173+
.count - 1 {
174+
Divider()
175+
.padding(.horizontal)
176+
}
177+
}
178+
}
179+
180+
// Load more section
181+
if result.hasNextPage {
182+
VStack(spacing: 12) {
183+
if result.isFetchingNextPage {
184+
HStack(spacing: 8) {
185+
ProgressView()
186+
.scaleEffect(0.8)
187+
Text("Loading more Pokemon...")
188+
.foregroundColor(.secondary)
189+
.font(.subheadline)
190+
}
191+
.padding()
192+
} else {
193+
Button("Load More Pokemon") {
194+
Task {
195+
_ = await result.fetchNextPage()
196+
}
197+
}
198+
.buttonStyle(.bordered)
199+
.padding()
200+
}
201+
}
202+
.frame(maxWidth: .infinity)
203+
.onAppear {
204+
// Auto-load when this view appears (infinite scrolling)
205+
if !result.isFetchingNextPage {
206+
Task {
207+
_ = await result.fetchNextPage()
208+
}
209+
}
210+
}
211+
} else {
212+
// End of list indicator
213+
VStack(spacing: 8) {
214+
Text("🎉")
215+
.font(.title)
216+
Text("You've seen all available Pokemon!")
217+
.font(.subheadline)
218+
.foregroundColor(.secondary)
219+
}
220+
.padding()
221+
.frame(maxWidth: .infinity)
222+
}
144223
}
145224
}
146225
.refreshable {

Example/swiftui-query-demo/Services/PokemonAPI.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ enum PokemonAPI {
7676
let url = URL(safeString: "https://pokeapi.co/api/v2/pokemon/\(id)")
7777
print("🔥 API CALL: Fetching Pokemon with ID \(id) from \(url)")
7878
do {
79-
try await Task.sleep(nanoseconds: UInt64(5 * 1_000_000_000))
79+
try await Task.sleep(nanoseconds: UInt64(1 * 1_000_000_000)) // Reduced delay for better UX
8080
let (data, _) = try await URLSession.shared.data(from: url)
8181
let pokemon = try JSONDecoder().decode(Pokemon.self, from: data)
8282
print("✅ API SUCCESS: Fetched Pokemon '\(pokemon.name)' (ID: \(id))")
@@ -101,6 +101,15 @@ enum PokemonAPI {
101101
}
102102
}
103103

104+
// MARK: - Infinite Query Support
105+
106+
/// Fetch Pokemon list page for infinite scrolling
107+
/// - Parameter pageParam: The offset for pagination (0 for first page, 20 for second, etc.)
108+
/// - Returns: PokemonList containing results for this page
109+
static func fetchPokemonPage(offset: Int) async throws -> PokemonList {
110+
return try await fetchPokemonList(limit: 20, offset: offset)
111+
}
112+
104113
static func searchPokemon(name: String) async throws -> Pokemon {
105114
let url = URL(safeString: "https://pokeapi.co/api/v2/pokemon/\(name.lowercased())")
106115
print("🔥 API CALL: Searching Pokemon with name '\(name)' from \(url)")

0 commit comments

Comments
 (0)