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.
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.
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.
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:
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}