carson-katri / swift-request

Declarative HTTP networking, designed for SwiftUI
MIT License
727 stars 41 forks source link

Question: Pagination #49

Closed ca13ra1 closed 3 years ago

ca13ra1 commented 3 years ago

I'm using the RickAndMorty API alongside Request just fine here. The API is limited to 20 items per request. The data includes the page count, next url and previous url. I attempted to use RequestChain but I can't seem to figure out the correct way to handle the Data type. The end goal is to load all characters from the API using Request in a simple List. I've only used RequestView and AnyRequest and not sure if I can do what I'm trying using AnyRequest or even RequestChain/RequestGroup, been stuck on this for days now.

data struct

struct RickAndMorty: Codable {
    let info: Info
    let results: [Result]
}

struct Info: Codable {
    let pages: Int /// number of pages 34
    let next: String? /// next url string
    let prev: String? /// previous url string
}

Failed chain request

RequestChain {
    Request.chained { (data, err) in
        Url("https://rickandmortyapi.com/api/character/")
        Method(.get)
    }
    Request.chained { (data, err) in
       let json = try? Json(data[0]!)
       return Url(json!["info"]["next"].string)
    }
}
.call { (data, errors) in
    let json = try? Json(data[0]!)
    print(json!)
}
carson-katri commented 3 years ago

Since you're using SwiftUI, you may want to try storing the paged results in an Array, then querying the next page when you reach the end. This should give you an infinite-scroll style. Let me know if that works for you.

struct ContentView : View {
    @State private var responses: [(page: Int, element: RickAndMorty)] = []

    var body: some View {
        NavigationView {
            List {
                // Loop over each page of results
                ForEach(responses, id: \.page) { response in
                    // Loop over each character. Enumerate the characters so we can identify the last one.
                    ForEach(Array(response.element.results.enumerated()), id: \.element.id) { character in
                        Text(character.element.name)
                            .onAppear {
                                // When the last item appears, load the next results.
                                guard character.offset == response.element.results.count - 1,
                                      let next = response.element.info.next,
                                      let pageString = next.components(separatedBy: "?page=").last,
                                      let nextPage = Int(pageString)
                                else { return }
                                loadPage(nextPage)
                            }
                    }
                }
            }
            .navigationTitle("Characters")
        }
        .onAppear{ loadPage(1) }
    }

    func loadPage(_ page: Int) {
        // Don't re-fetch any data.
        guard !responses.contains(where: { $0.page == page }) else { return }
        // Get the next page of characters
        AnyRequest<RickAndMorty> {
            Url("https://rickandmortyapi.com/api/character")
            Query(page == 1 ? [:] : ["page": "\(page)"])
        }
        .onObject { response in
            DispatchQueue.main.async {
                // Add the results in the correct spot if they came in late for some reason.
                responses.insert((page, response), at: page - 1)
            }
        }
        .onError {
            print("Failed with: \($0.localizedDescription)")
        }
        .call()
    }
}
ca13ra1 commented 3 years ago

Getting Task <192B2F72-4623-44D4-8485-D206EF287E44>.<1> finished with error [-1007] Error Domain=NSURLErrorDomain Code=-1007 "too many HTTP redirects" UserInfo={NSLocalizedDescription=too many HTTP redirects, NSErrorFailingURLStringKey=https://rickandmortyapi.com/api/character, NSErrorFailingURLKey=https://rickandmortyapi.com/api/character, _NSURLErrorRelatedURLSessionTaskErrorKey=( "LocalDataTask <192B2F72-4623-44D4-8485-D206EF287E44>.<1>" ), _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <192B2F72-4623-44D4-8485-D206EF287E44>.<1>, NSUnderlyingError=0x6000034d7ae0 {Error Domain=kCFErrorDomainCFNetwork Code=-1007 "(null)"}} Failed with: too many HTTP redirects

carson-katri commented 3 years ago

I actually think that's an issue with their API. I would often get the same error in Safari and Postman

ca13ra1 commented 3 years ago

Thank you for helping though, much appreciated.

ca13ra1 commented 3 years ago

Adding UserAgent fixed the too many HTTP redirects. But no other data is loaded when it reaches the last indexed value

Method(.get)
Header.UserAgent(.firefoxMac)
carson-katri commented 3 years ago

Is it not running loadPage at all, or is it throwing an error?

ca13ra1 commented 3 years ago

It does actually call loadPage but nothing happens. It seems the Query string isn't accepted by the API. I modified the Url to Url("https://rickandmortyapi.com/api/character/?page=\(page == 1 ? 1 : page)") but can't get the next url to load.

ca13ra1 commented 3 years ago

Finally working, thank you for the help! Closing 😁