Pagination
Apollo Pagination provides a convenient and easy way to interact with and watch paginated APIs. It provides a flexible and powerful way to interact with paginated data, and works with both cursor-based and offset-based pagination. Its key features include:
Watching paginated data
Forward, reverse, and bidirectional pagination support
Multi-query pagination support
Support for custom model types
Apollo Pagination provides two classes to interact with paginated endpoints: GraphQLQueryPager
and AsyncGraphQLQueryPager
. They have very similar APIs, but the latter supports async
/await
for use in asynchronous contexts.
Apollo Pagination is its own Swift Package, in order to use the pagination functionality you will need to include the apollo-ios-pagination SPM package in your project along with apollo-ios
Using a GraphQLQueryPager
The GraphQLQueryPager
class is intended to be a simple, flexible, and powerful way to interact with paginated data. While it has a standard initializer, it is recommended to use the convenience initializers, which simplify the process of creating a GraphQLQueryPager
instance.
In this example, a GraphQLQueryPager
is initialized that paginates a single query in the forward direction with a cursor-based pagination:
1let initialQuery = MyQuery(first: 10, after: nil)
2let pager = GraphQLQueryPager(
3 client: client,
4 initialQuery: initialQuery,
5 extractPageInfo: { data in
6 CursorBasedPagination.Forward(
7 hasNext: data.values.pageInfo.hasNextPage ?? false,
8 endCursor: data.values.pageInfo.endCursor
9 )
10 },
11 pageResolver: { page, paginationDirection in
12 // As we only want to support forward pagination, we can return `nil` for reverse pagination
13 switch paginationDirection {
14 case .next:
15 return MyQuery(first: 10, after: page.endCursor ?? .none)
16 case .previous:
17 return nil
18 }
19 }
20)
In this example, the GraphQLQueryPager
instance is initialized with an extractPageInfo
closure which extracts the pagination information from the query result and an pageResolver
closure, which provides the next pagination query to be executed. The GraphQLQueryPager
instance can then be used to fetch the paginated data, and to watch for changes to the paginated data.
Whenever the pager needs to load a new page, it will call the extractPageInfo
closure, passing in the data returned from the last page queried. Your implementation of extractPageInfo
should return a PaginationInfo
value that can be used to query the next page. Then the pager calls pageResolver
, passing in the PaginationInfo
that was provided by the extractPageInfo
closure. Your implementation of pageResolver
should then return a query for the next page using the given PaginationInfo
.
We could similarly support forward offset-based pagination by supplying OffsetPagination.Forward
instead of CursorBasedPagination.Forward
to the extractPageInfo
closure.
Using an AsyncGraphQLQueryPager
The AsyncGraphQLQueryPager
class is similar to the GraphQLQueryPager
class, but it supports async
/await
for use in asynchronous contexts.
In this example, an AsyncGraphQLQueryPager
is initialized that paginates a single query in the forward direction with cursor-based pagination:
1let initialQuery = MyQuery(first: 10, after: nil)
2let pager = AsyncGraphQLQueryPager(
3 client: client,
4 initialQuery: initialQuery,
5 extractPageInfo: { data in
6 CursorBasedPagination.Forward(
7 hasNext: data.values.pageInfo.hasNextPage ?? false,
8 endCursor: data.values.pageInfo.endCursor
9 )
10 },
11 pageResolver: { page, paginationDirection in
12 // As we only want to support forward pagination, we can return `nil` for reverse pagination
13 switch paginationDirection {
14 case .next:
15 return MyQuery(first: 10, after: page.endCursor ?? .none)
16 case .previous:
17 return nil
18 }
19 }
20)
Note that it is initialized in an identical manner to GraphQLQueryPager
, with the same parameters.
Subscribing to results
The GraphQLQueryPager
and AsyncGraphQLQueryPager
classes can fetch data, but the caller must subscribe to the results in order to receive the data. A subscribe
method is provided which takes a closure that is called whenever the pager fetches a new page of data. The subscribe
method is a convenience method that ensures that the closure is called on the main thread.
1// Guaranteed to run on the main thread
2pager.subscribe { result in
3 switch result {
4 case .success(let data):
5 // Handle the data
6 case .failure(let error):
7 // Handle the error
8 }
9}
Both the GraphQLQueryPager
and AsyncGraphQLQueryPager
are also Combine Publisher
s. As such, all Publisher
methods are available, such as sink
, assign
, map
, filter
, and so on.
1// Can run on any thread
2pager.sink { result in
3 switch result {
4 case .success(let data):
5 // Handle the data
6 case .failure(let error):
7 // Handle the error
8 }
9}
Fetching Data
The GraphQLQueryPager
class provides several methods to fetch paginated data: fetch
, refetch
, loadNext
, loadPrevious
, and loadAll
.
fetch
: Fetches the first page of data. Must be called beforeloadNext
orloadPrevious
can be called. Provides a completion handler that allows the caller to be notified when the fetch operation is complete.refetch
: Cancels all in-flight fetch operations and resets the pager to its initial state. Fetches the first page of data. Provides a completion handler that allows the caller to be notified when the fetch operation is complete.loadNext
: Fetches the next page of data. Can only be called afterfetch
has been called. Provides a completion handler that allows the caller to be notified when the operation is complete, with an optionalError?
parameter that contains any usage errors that may have occurred.loadPrevious
: Fetches the previous page of data. Can only be called afterfetch
has been called. Provides a completion handler that allows the caller to be notified when the operation is complete, with an optionalError?
parameter that contains any usage errors that may have occurred.loadAll
: Fetches all pages of data. If no initial page is detected, it will first callfetch
to fetch the first page of data. Will continue to fetch all pages until aPageInfo
object indicates that there are no more pages to fetch. This function is compatible with forward, reverse, and bidirectional pagination. Provides a completion handler that allows the caller to be notified when the operation is complete, with an optionalError?
parameter that contains any usage errors that may have occurred.
The AsyncGraphQLQueryPager
class provides the same methods as async
functions, but without completion handlers, as they are not needed in an asynchronous context.
Cancelling ongoing requests
The GraphQLQueryPager
class provides a reset
method, which can be used to cancel all in-flight fetch operations and stop watching for changes to cached data. This does not cancel subscriber's to the pager. Once the pager's state is reset, you can call fetch
to being receiving updates again and existing subscribers will continue to receive updates.
Error handling
There are two broad categories of errors that the GraphQLQueryPager
class can throw: errors as a result of network operations, or errors as a result of usage. A network error is exposed to the user when the pager encounters a network error, such as a timeout or a connection error, via the Result
that is passed to the subscriber. Usage errors, such as cancellations or attempting to begin a new fetch while a load is in progress, are thrown as PaginationError
types (for AsyncGraphQLQueryPager
) or exposed as callbacks in each fetch method (for GraphQLQueryPager
). Note that GraphQLQueryPager
's callbacks are optional, and the user can choose to ignore them.
Usage errors in GraphQLQueryPager
The loadNext
, loadPrevious
, and loadAll
methods all have a completion handler that is called with a Result
type. This Result
type can contain either the paginated data or a PaginationError
type. Common pagination errors are attempting to fetch while there is already a load in progress, or attempting to fetch a previous or next page without first calling fetch
.
1// Attempting to fetch a previous page without first calling `fetch`
2pager.loadNext { error in
3 if let error {
4 // Handle error
5 } else {
6 // We have no error, and are finished with our fetch operation
7 }
8}
9
10// Note that we can silently ignore the error
11pager.loadNext()
Usage errors in AsyncGraphQLQueryPager
The AsyncGraphQLQueryPager
class can throw a PaginationError
type directly, as opposed to exposing it via a completion handler. As an inherently asynchronous type, the AsyncGraphQLQueryPager
can intercept an error thrown within a Task
and forward it to the caller.
1// Attempting to fetch a previous page without first calling `fetch`
2try await pager.loadNext()
3
4// Similarly, we can silently ignore the error
5try? await pager.loadNext()