The Next Page: Building Infinite Scroll with SwiftUI

Larry Tran
Whatnot Engineering
4 min readMay 31, 2022

How we built infinite scroll on iOS with SwiftUI & async/await for the Whatnot Feeds

This is part of the “Feeds with GraphQL” series. Rami has written a great article about the inspiration here.

At Whatnot we are guided by our core principles and one of our principles is moving uncomfortably fast. The tools we use should reflect our principles so we decided to use SwiftUI to build the Whatnot homefeed on iOS. Our goal was to rebuild a home feed that could be dynamically driven and personalized from the backend with infinite scroll. In this post we will be diving into how we built the core models for infinite scroll.

Pageable Protocol

Before we can start doing anything with paging, we have to define what a page is. At its simplest, a page is a partial list of content (array of values) and metadata about that partial list (PageInfo). Our backend uses a GraphQL api, so this metadata is defined using the Relay’s PageInfo specification. For our use case, PageInfo tells us where our current page ends (endCursor) and if there is a next page (hasNextPage).

Now that we know what a page is, we can request one. There are two key parameters that our page requests need: The size of the page, and where the page should start (the current page’s endCursor). Both of these parameters are typically optional, so if neither are provided, the first page at a default size will be fetched. Knowing which information is needed to request a page, we can now define a generic interface to power the fetching portion of our paged UI. This brings us to the Pageable protocol.

There is only one function in the Pageable Protocol which ties together everything we have discussed so far.

loadPage takes the current page (we use its endCursor value to specify where the next page starts) and a page size (size), returning the next slice of values and its corresponding PageInfo.

In addition to the loadPage function, Pageable also specifies `Value` as an associatedType conforming to Identifiable and Hashable. The Identifiable and Hashable constraints allow the page’s content to be diffed and easily rendered in a list withinin SwiftUI.

Paging View Model

The next part is building our view model for our SwiftUI view. This will encapsulate the following properties:

  • source (Pageable)
  • pageSize (number of objects to fetch)
  • threshold (position of the last object where the next page fetch will be triggered)
  • pageInfo (details of the current page and next)
  • state (an enum to hold the the current state of the view model)
  • items (the source of truth for all our objects)

Paging View Model: PagingState

We also need to know the internal state of the view model to prevent fetching the same page multiple times. We can use an enum to manage the internal state of the paging view model to prevent this.

Paging View Model: onAppear

The next part is defining how our view model fetches the next page from SwiftUI. We first need to know where the user is in the current items array. We can use the onAppear SwiftUI view modifier to notify our view model when an item is currently visible, this will help us understand the position and allow us to kick off a request for the next page if necessary.

In our view model we define our onItemAppear function with a series of early returns before kicking off a request for the next page.

Let’s break this down:

  1. We’ve reached the end of the page and can no longer page
  2. We are currently loading the next or first page
  3. No index is found
  4. The index of the our model has not reached our threshold
  5. Requirements have been met; kick off a request for the next page 🎉

Paging View Model: loadMoreItems

Let’s now define our loadMoreItems function.

Let’s break this down:

  1. Ask the source for the page given the pageInfo and pageSize
  2. If we’ve set a new currentTask we shouldn’t continue here
  3. We have our new items set our items based on the state of the view model
  4. Publish our changes to SwiftUI and reset our state so we fetch the next page if needed
  5. Publish any errors back to SwiftUI

Recap

We’ve defined a protocol Pageable that provides a source for our PagingViewModel. Our PagingViewModel gets triggered by the onAppear SwiftUI view modifier. We check the index of the item from the view modifier and determine if the item has met our threshold requirement and if the state is ready to make a page request. Once we have a page response we publish the set of items back into SwiftUI and wait for the next request.

Here is a demo of what that looks like:

Key Takeaways

  • Pageable has allowed us to create many sources that can be paged
  • updating state to SwiftUI views is as simple as setting a property
  • PagingState makes our PagingViewModel easy to reason with

We’re just getting started on our iOS journey. Interested in building with us? We’re hiring!

Special Thanks to Alex Chase and Rami Khalaf

--

--