Directional Pagination


GraphQLQueryPager supports pagination in both the forward and reverse direction, as well as both at once.

Forward Pagination

Forward pagination is the most common form of pagination. It is used to fetch the next n items in a list. While we have many options depending on our requirements -- whether we use one query or two, whether we want to use a cursor or an offset, whether we want to transform the results, etc. -- we will examine using a cursor with a single query.

Swift
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)

Reverse Pagination

Reverse pagination is used to fetch the previous n items in a list. While we have many options depending on our requirements -- whether we use one query or two, whether we want to use a cursor or an offset, whether we want to transform the results, etc. -- we will examine using a cursor with a single query.

Swift
1let initialQuery = MyQuery(first: 10, after: nil)
2let pager = GraphQLQueryPager(
3    client: client,
4    initialQuery: initialQuery,
5    extractPageInfo: { data in
6        CursorBasedPagination.Reverse(
7            hasNext: data.values.pageInfo.hasPrevious ?? false,
8            endCursor: data.values.pageInfo.startCursor
9        )
10    },
11    pageResolver: { page, paginationDirection in
12        // As we only want to support reverse pagination, we can return `nil` for forward pagination
13        switch paginationDirection {
14        case .next:
15            return nil
16        case .previous:
17            return MyQuery(first: 10, before: page.startCursor ?? .none)
18        }
19    }
20)

Bi-directional Pagination

Bi-directional pagination is used to fetch the next n items in a list, as well as the previous n items in a list. Given that we can fetch in both directions, the implication is that the initial query fetched is at neither the head nor tail of the list of results. For this example, we will examine using a cursor with a single query.

Swift
1let pager = GraphQLQueryPager(
2    client: client,
3    initialQuery: MyQuery(first: 10, after: nil),
4    extractPageInfo: { data in
5        CursorBasedPagination.Bidirectional(
6            hasNext: data.values.pageInfo.hasNextPage ?? false,
7            endCursor: data.values.pageInfo.endCursor,
8            hasPrevious: data.values.pageInfo.hasPreviousPage ?? false,
9            startCursor: data.values.pageInfo.startCursor
10        )
11    },
12    pageResolver: { page, direction in
13        switch direction {
14        case .next:
15            return MyQuery(first: 10, after: page?.endCursor ?? .none)
16        case .previous:
17            return MyQuery(first: 10, before: page?.startCursor ?? .none)
18        }
19    }
20)

Custom Configuration

Generally, it's recommended to use a convenience initializer to create a configured GraphQLQueryPager. However, if you need to customize the configuration, you can use the init method directly. If your application only uses a specific type of pagination, it is simple to declare a custom GraphQLQueryPager convenience initializer that can be used throughout your application. This allows you to easily instantiate a pager without worrying about cases that your application does not support. If your pagination scheme only supports forward pagination and offsets, you can declare a custom GraphQLQueryPager convenience initializer that only supports forward pagination and offsets. In this example, we declare two initializers for forward offset pagination -- one with a transform, and one without:

Swift
1extension GraphQLQueryPager {
2  static func makeForwardOffsetQueryPager<InitialQuery: GraphQLQuery>(
3    client: ApolloClientProtocol,
4    watcherDispatchQueue: DispatchQueue = .main,
5    queryProvider: @escaping (OffsetPagination.Forward?) -> InitialQuery,
6    extractPageInfo: @escaping (InitialQuery.Data) -> OffsetPagination.Forward
7  ) -> GraphQLQueryPager where Model == PaginationOutput<InitialQuery, InitialQuery> {
8    GraphQLQueryPager.init(
9      client: client,
10      initialQuery: queryProvider(nil),
11      watcherDispatchQueue: watcherDispatchQueue,
12      extractPageInfo: pageExtraction(transform: extractPageInfo),
13      pageResolver: { page, direction in
14        guard direction == .next else { return nil }
15        return queryProvider(page)
16      }
17    )
18  }
19
20  static func makeForwardOffsetQueryPager<InitialQuery: GraphQLQuery, T>(
21    client: ApolloClientProtocol,
22    watcherDispatchQueue: DispatchQueue = .main,
23    queryProvider: @escaping (OffsetPagination.Forward?) -> InitialQuery,
24    extractPageInfo: @escaping (InitialQuery.Data) -> OffsetPagination.Forward,
25    transform: @escaping (InitialQuery.Data) throws -> Model
26  ) -> GraphQLQueryPager where Model: RangeReplaceableCollection, T == Model.Element {
27    GraphQLQueryPager.init(
28      client: client,
29      initialQuery: queryProvider(nil),
30      watcherDispatchQueue: watcherDispatchQueue,
31      extractPageInfo: pageExtraction(transform: extractPageInfo),
32      pageResolver: { page, direction in
33        guard direction == .next else { return nil }
34        return queryProvider(page)
35      },
36      initialTransform: transform,
37      pageTransform: transform
38    )
39  }
40}
41
42private func pageExtraction<InitialQuery: GraphQLQuery, P: PaginationInfo, T>(
43  transform: @escaping (InitialQuery.Data) -> P
44) -> (PageExtractionData<InitialQuery, InitialQuery, T>) -> P {
45  { extractionData in
46    switch extractionData {
47    case .initial(let value, _), .paginated(let value, _):
48      return transform(value)
49    }
50  }
51}
52
53private func pageExtraction<InitialQuery: GraphQLQuery, PaginatedQuery: GraphQLQuery, P: PaginationInfo, T>(
54  initialTransform: @escaping (InitialQuery.Data) -> P,
55  paginatedTransform: @escaping (PaginatedQuery.Data) -> P
56) -> (PageExtractionData<InitialQuery, PaginatedQuery, T>) -> P {
57  { extractionData in
58    switch extractionData {
59    case .initial(let value, _):
60      return initialTransfom(value)
61    case .paginated(let value, _):
62      return paginatedTransform(value)
63    }
64  }
65}
Feedback

Edit on GitHub

Forums