Skip to Content
Course content

196: Handling Pagination in Network Requests

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I actually track which page I'm on without making a mess of my state?

When you're dealing with a large dataset—say, a list of 1,000 movies from a database—you can't just grab everything at once. You'll kill the user's data plan and likely crash your app's memory. I usually handle this by introducing a state manager or a ViewModel that tracks the currentPage.

Most APIs use either an offset (how many items to skip) or a page number. I prefer page numbers because they're easier to reason about. Here is how I typically structure the request logic:

struct MovieResponse: Codable {
    let page: Int
    let results: [Movie]
    let totalPages: Int
}

class MovieViewModel: ObservableObject {
    @Published var movies: [Movie] = []
    private var currentPage = 1
    private var totalPages = 1

    func fetchMovies() async {
        // We pass the currentPage to the API call
        do {
            let response = try await NetworkManager.shared.getMovies(page: currentPage)
            self.movies.append(contentsOf: response.results)
            self.totalPages = response.totalPages
            self.currentPage += 1 
        } catch {
            print("Handle error here")
        }
    }
}

Notice that I'm using append(contentsOf:). If you just assign the result to your array, you'll wipe out the movies from page one every time you load page two. That's a common mistake I see juniors make.

How do I stop the app from firing five requests for the same page at once?

If you trigger a fetch when the user scrolls to the bottom of a list, you're going to hit a problem: the scroll event fires constantly. If your network request takes a second to return, the user might trigger the "load more" logic ten times in that window. You'll end up with duplicate data and a very angry API server.

The fix is a simple guard flag. I call it isLoading. You check if it's true at the start of the function and set it to true immediately. Only set it back to false once the request is fully finished.

private var isLoading = false

func fetchMovies() async {
    guard !isLoading else { return } // Stop if we're already fetching
    
    isLoading = true
    defer { isLoading = false } // Ensures this runs even if the function throws
    
    do {
        let response = try await NetworkManager.shared.getMovies(page: currentPage)
        // ... update movies array ...
    } catch {
        // handle error
    }
}

I use defer here because it's cleaner than writing isLoading = false in both the success and the catch blocks. It's a little Swift trick that keeps the logic tight.

How do I know when I've actually hit the end of the list?

You don't want your app to keep trying to fetch page 11 if the API only has 10 pages of data. It's a waste of resources and creates a poor user experience if you have a loading spinner that just spins forever because the server is returning empty arrays.

The best way to handle this is to track a canLoadMore boolean. I derive this from the totalPages value returned by the API response. If your current page equals or exceeds the total pages, you flip the switch to false and stop making calls.

private var canLoadMore = true

func fetchMovies() async {
    guard canLoadMore && !isLoading else { return }
    
    isLoading = true
    defer { isLoading = false }

    do {
        let response = try await NetworkManager.shared.getMovies(page: currentPage)
        self.movies.append(contentsOf: response.results)
        
        // If we've reached the end, stop future requests
        if currentPage >= response.totalPages {
            canLoadMore = false
        } else {
            currentPage += 1
        }
    } catch {
        // handle error
    }
}

Pro tip: In your UI, you can use this canLoadMore flag to hide the loading spinner at the bottom of the list, letting the user know they've truly seen everything.




📋 Practical Task

Exercise: Implementing the Infinite Scroll Guard for a Movie Feed

You have been provided with a MovieViewModel and a MovieAPI client. Currently, the loadNextPage() function is causing duplicate entries in the movie list because it triggers multiple times during scrolling.

Your Task:

  • Add an isLoading boolean flag to the ViewModel to prevent concurrent network requests.
  • Implement a canLoadMore check that compares the currentPage against the totalPages returned in the MovieResponse.
  • Modify the loadNextPage() function to guard against both isLoading == true and canLoadMore == false before proceeding with the API call.
  • Ensure the isLoading flag is reset to false regardless of whether the network request succeeds or fails.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.