When working with a large dataset, it is usually not a good idea to download all the records in a single request. Not only does this increase the amount of data transferred over the network, but it can also negatively impact the performance of the application.
One way to solve this problem is through pagination, where the server returns a small subset of records instead of the entire dataset. On the client side, we can take this a step further by implementing infinite scrolling, which allows us to fetch additional records as the user scrolls through the list.
In this article, we will implement infinite scrolling in our SwiftUI hotel application using an ExpressJS backend. We will also learn how to handle pagination when the user changes the sorting and filtering options.
This article builds on our previous implementations of server-side sorting and filtering in SwiftUI. If you have not followed along with those articles, I recommend checking them out before continuing.
Implementing Server-Side Sorting in SwiftUI — Learn how to sort hotels using an ExpressJS backend and manage sorting preferences in SwiftUI.
Implementing Server-Side Filtering in SwiftUI — Learn how to filter hotels based on amenities and combine filtering with sorting to display relevant results.
In this article, we will extend the same hotel application by adding pagination and infinite scrolling, allowing users to load additional hotels as they scroll through the list.
For infinite scrolling to work, we first need to implement pagination on the server. The server is responsible for extracting the page and limit query string parameters and returning a subset of hotels based on those values.
Let’s start by implementing the paginateHotels function, as shown below:
const paginateHotels = (pageNumber, pageLimit, hotels) => {
let startIndex = (pageNumber - 1) * pageLimit
let endIndex = startIndex + pageLimit
const paginatedHotels = hotels.slice(startIndex, endIndex)
return paginatedHotels
}
The implementation is quite straightforward. We calculate the starting index based on the requested page number and page limit. We then use the JavaScript slice function to return only the hotels that fall within that range.
For example, consider the following URL:
http://localhost:8080/api/hotels?page=3&limit=5
In this case, we are requesting page 3 with a limit of 5 hotels per page. This means the server will return records 11–15, assuming there are enough records available.
Now, let’s take a look at the complete implementation of the endpoint:
app.get("/api/hotels", (req, res) => {
// get the sort options
const { sort, page = 1, limit = 10 } = req.query
const pageNumber = Number(page)
const pageLimit = Math.min(Number(limit), 50)
let filteredHotels = filterHotels(hotels, req.query)
filteredHotels = sortHotels(filteredHotels, sort)
// paginate
filteredHotels = paginateHotels(pageNumber, pageLimit, filteredHotels)
res.json(filteredHotels)
})
const paginateHotels = (pageNumber, pageLimit, hotels) => {
let startIndex = (pageNumber - 1) * pageLimit
let endIndex = startIndex + pageLimit
const paginatedHotels = hotels.slice(startIndex, endIndex)
return paginatedHotels
}
Notice that we first apply the filters and sorting options before performing pagination. This is important because we want to paginate the filtered and sorted results, not the original collection of hotels.
We also limit the maximum number of hotels returned in a single request to 50. This prevents the client from requesting an unnecessarily large number of records.
For a production application, we should also validate that page and limit are positive integers before using them.
Now that our server supports pagination, let’s move to the client side and see how we can use it to implement infinite scrolling in SwiftUI.
Infinite scrolling works by downloading additional data as the user scrolls down the screen. You can think of it as on-demand loading, where we only fetch more hotels when they are needed instead of downloading the entire collection in the initial request.
Infinite scrolling is commonly used by social media applications like Twitter, Facebook, Instagram, and TikTok, where additional content is continuously loaded as the user scrolls through the feed.
The first thing we need to figure out is when to make the next request to the server. Ideally, we want to fetch additional hotels when the user reaches the end of the currently loaded list.
One way to accomplish this is by comparing the current hotel’s ID with the ID of the last hotel in the collection. If both IDs match, we know that the last hotel cell has appeared, and we can request the next page.
This is shown in the implementation below:
ForEach(hotelStore.hotels) { hotel in
HotelCellView(hotel: hotel)
.task {
if hotel.id == hotelStore.hotels.last?.id {
await loadHotels()
}
}
}
As you can see, we have attached a task modifier to HotelCellView. SwiftUI executes the task when the view appears. Inside the task, we check whether the current hotel is the last hotel in the collection. If it is, we call the loadHotels function to fetch the next page from the server.
Keep in mind that the task modifier is tied to the view’s lifecycle and may execute more than once as views appear again. This means our store should prevent duplicate requests.
When using a List, SwiftUI creates and manages rows lazily, so it does not need to create every row at once. This helps improve performance when displaying large collections of hotels. However, it does not mean that only the rows currently visible on the screen are instantiated.
Now, let’s take a look at the updated loadHotels function in HotelStore.
func loadHotels(sortOption: SortOption? = nil, filters: Set<FilterOption>? = nil, pageSize: Int = 10) async throws {
let queryChanged = (self.sortOption != sortOption) || (self.filters != filters) || (self.pageSize != pageSize)
let page = queryChanged ? 1: currentPage
guard queryChanged || hasMoreHotels else { return }
let resource = Resource(url: Constants.URLs.hotels(sort: sortOption, filters: filters, page: page, pageSize: pageSize), responseType: [Hotel].self)
print(resource.url)
let newHotels = try await httpClient.fetch(resource: resource)
if page == 1 {
hotels = newHotels
} else {
hotels.append(contentsOf: newHotels)
}
self.sortOption = sortOption
self.filters = filters
currentPage = page + 1
hasMoreHotels = newHotels.count == pageSize
}
There are a few interesting things happening in this implementation.
First, we check whether the sorting, filtering or page size have changed. If they have, we reset the page number to 1. This is important because the new sorting, filtering or page size options may produce a completely different collection of hotels, and we want to start displaying those results from the beginning.
Next, we check the hasMoreHotels property to determine whether we should make another request. If there are no additional hotels to fetch and the query has not changed, we simply return without making a network request.
Once the server returns the hotels, we check the page number. If the page number is 1, we replace the existing hotels with the new results. Otherwise, we append the newly downloaded hotels to the existing collection.
Finally, we increment currentPage so the next request fetches the following page. We also update hasMoreHotels by comparing the number of returned hotels with the requested page size.
If the server returns fewer hotels than the requested page size, we assume that there are no additional hotels available.
There are also two improvements worth considering before using this implementation in a production application. The first is to track whether a request is already in progress using an isLoading property. This prevents multiple requests from being triggered when the last hotel cell appears more than once. The second is to include pageSize when checking whether the query has changed. Otherwise, changing the page size could result in skipped or duplicated hotels.
With these pieces in place, we have the basic functionality needed to support infinite scrolling in our SwiftUI application.
You can download the source code from GitHub: https://github.com/azamsharpschool/HotelApp
Infinite scrolling is a great way to load data on demand without requiring users to manually navigate between pages. By implementing pagination on the server and detecting when the user reaches the end of the list in SwiftUI, we can create a seamless scrolling experience. We also learned how to reset pagination when sorting or filtering options change, ensuring that the user always sees the correct results. This approach is not limited to hotels and can be applied to products, recipes, social media feeds, or any application that displays a large collection of records.
Become an AzamSharp School member and get access to more than 250 hours of practical courses covering SwiftUI, SwiftData, testing, architecture, AI, machine learning, and more.
Your membership also includes access to AzamSharp books, live workshops, office hours, and new content added regularly.