Edited from an earlier post to include a working subset of code: I'm apparently not understanding how .onAppear works in SwiftUI with respect to Views inside of Navigation Links. I'm trying to use it to get paged JSON (in this case from the Pokemon API at pokeapi.co.
A minimal reproducible bit of code is below. As I scroll through the list, I see all of the Pokemon names for the first page print out & when I hit the last Pokemon on the page, I get the next page of JSON (I can see the # jump from 20, one page, to 40, two pages). My API call seems to be working fine & I'm loading a second page of Pokemon. I see their names appear & they print to the console when running in the simulator. However, even though the JSON is properly loaded into my list & I go from 20 to 40 Pokemon - a correct array of the first two pages - as I scroll past 40 it looks like the third page has loaded, creatures through 60 are visible in the List, but the console only occasionally shows an index name printing (also shown a sample of the output, below, note the values printing past 40 don't all show). The .onAppear doesn't seem to be firing as I expected past the 40th element, even though I can see 60 names showing up in the List. I was hoping to use .onAppear to detect when a new page needs to load & call it, but this method doesn't seem sound. Any hints why .onAppear isn't working as I expect & how I might more soundly handle recognizing when I need to load the next page of JSON? Thanks!
struct Creature: Hashable, Codable {
var name: String
var url: String
}
@MainActor
class Creatures: ObservableObject {
private struct Returned: Codable {
var count: Int
var next: String?
var results: [Creature]
}
var count = 0
var urlString = "https://pokeapi.co/api/v2/pokemon/"
@Published var creatureArray: [Creature] = []
var isFetching = false
func getData() async {
guard !isFetching else { return }
isFetching = true
print(" We are accessing the url \(urlString)")
// Create a URL
guard let url = URL(string: urlString) else {
print(" ERROR: Could not create a URL from \(urlString)")
isFetching = false
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
if let returned = try? JSONDecoder().decode(Returned.self, from: data) {
self.count = returned.count
self.urlString = returned.next ?? ""
DispatchQueue.main.async {
self.creatureArray = self.creatureArray + returned.results
}
isFetching = false
} else {
isFetching = false
print(" JSON ERROR: Could not decode returned data.")
}
} catch {
isFetching = false
print(" ERROR: Could not get URL from data at \(urlString). \(error.localizedDescription)")
}
}
}
struct ContentView: View {
@StateObject var creatures = Creatures()
var body: some View {
NavigationStack {
List {
ForEach(0..<creatures.creatureArray.count, id: \.self) { index in
NavigationLink {
Text(creatures.creatureArray[index].name)
} label: {
Text("\(index+1). \(creatures.creatureArray[index].name)")
}
.onAppear() {
print("index = \(index+1)")
if index == creatures.creatureArray.count-1 && creatures.urlString.hasPrefix("http") {
Task {
await creatures.getData()
}
}
}
}
}
.toolbar {
ToolbarItem (placement:.status) {
Text("\(creatures.creatureArray.count) of \(creatures.count)")
}
}
}
.task {
await creatures.getData()
}
}
}
Here's a sample of the output. The triple dots simply indicate order printed as expected:
We are accessing the url https://pokeapi.co/api/v2/pokemon/
index = 1
index = 2
index = 3
…
index = 37
index = 38
index = 39
index = 40
We are accessing the url https://pokeapi.co/api/v2/pokemon/?offset=40&limit=20
index = 41
index = 44
Try my fully functional example code that fetches the pokemons data as required.
The code gets the server response with the
resultswhen thePokeListViewfirst appears (in .task {...}). Then, as the user scrolls to the bottom of the current list, another page is fetched, until all data is presented.The new page fetching is triggered by checking for the last creature id displayed and if more data is available. This is the crux of the paging. Note, you can adjust to trigger before the last creature is displayed.
As the user tap on any one of the creatures name, the details view is presented. As the
PokeDetailsViewappears, the details are fetched from the server or from cache. This alleviates the server burden.The
ApiServicemanages all server processing. With this approach you are not fetching all the details before hand, only as required.Since you are fetching data from a remote server, there will be times when you will see the progress view, as it takes somethimes to download the data.