8. Paginate results


As you might have noticed, the object returned from the LaunchListQuery is a LaunchConnection. This object has a list of launches, a pagination cursor, and a boolean to indicate whether more launches exist.

When using a cursor-based pagination system, it's important to remember that the cursor gives you a place where you can get all results after a certain spot, regardless of whether more items have been added in the interim.

In the previous section, you hardcoded the SMALL size argument directly in the GraphQL query, but you can also define arguments programmatically using variables. You will use them here to implement pagination.

Add a cursor variable

In LaunchList.graphql, add a cursor variable. In GraphQL, variables are prefixed with the dollar sign.

GraphQL
LaunchList.graphql
1query LaunchList($cursor: String) { # highlight-line
2  launches(after: $cursor) { # highlight-line
3    hasMore
4    cursor
5    launches {
6      id
7      site
8      mission {
9        name
10        missionPatch(size: SMALL)
11      }
12    }
13  }
14}

Now re-run code generation to update the GraphQL code.

You can experiment with GraphQL variables in Sandbox Explorer by using the pane under the main body of the operation named Variables. If you omit the $cursor variable, the server returns data starting from the beginning:

Explorer variables

Update LaunchListViewModel to use cursor

First, you need to hang on to the most recently received LaunchConnection object.

Add a variable to hold on to this object, as well as a variable for the most recent request, at the top of the LaunchListViewModel.swift file near your launches variable:

Swift
LaunchListViewModel.swift
1@Published var launches = [LaunchListQuery.Data.Launches.Launch]()
2@Published var lastConnection: LaunchListQuery.Data.Launches? // highlight-line
3@Published var activeRequest: Cancellable? // highlight-line
4@Published var appAlert: AppAlert?
5@Published var notificationMessage: String?

Next, let's update our loadMoreLaunches() method to use the cursor property as well as manage the lastConnection and activeRequest properties:

Swift
LaunchListViewModel.swift
1private func loadMoreLaunches(from cursor: String?) { // highlight-line
2    self.activeRequest = Network.shared.apollo.fetch(query: LaunchListQuery(cursor: cursor ?? .null)) { [weak self] result in // highlight-line
3        guard let self = self else {
4            return
5        }
6
7        self.activeRequest = nil // highlight-line
8
9        switch result {
10        case .success(let graphQLResult):
11            if let launchConnection = graphQLResult.data?.launches {
12                self.lastConnection = launchConnection// highlight-line
13                self.launches.append(contentsOf: launchConnection.launches.compactMap({ $0 }))
14            }
15
16            if let errors = graphQLResult.errors {
17                self.appAlert = .errors(errors: errors)
18            }
19        case .failure(let error):
20            self.appAlert = .errors(errors: [error])
21        }
22    }
23}

Now implement the loadMoreLaunchesIfTheyExist() method to check if there are any launches to load before attempting to load them. Replace the TODO with the following code:

Swift
LaunchListViewModel.swift
1func loadMoreLaunchesIfTheyExist() {
2    guard let connection = self.lastConnection else {
3        self.loadMoreLaunches(from: nil)
4        return
5    }
6
7    guard connection.hasMore else {
8        return
9    }
10
11    self.loadMoreLaunches(from: connection.cursor)
12}

Update UI Code

Next, go to LaunchListView and update our task to call the newly implemented loadMoreLaunchesIfTheyExist() method:

Swift
LaunchListView.swift
1.task {
2    viewModel.loadMoreLaunchesIfTheyExist() // highlight-line
3}

Now update the List to optionally add a button to load more launches at the end of the list:

Swift
LaunchListView.swift
1List {
2    ForEach(0..<viewModel.launches.count, id: \.self) { index in
3        LaunchRow(launch: viewModel.launches[index])
4    }
5    if viewModel.lastConnection?.hasMore != false { // highlight-line
6        if viewModel.activeRequest == nil { // highlight-line
7            Button(action: viewModel.loadMoreLaunchesIfTheyExist) { // highlight-line
8                Text("Tap to load more") // highlight-line
9            } // highlight-line
10        } else { // highlight-line 
11            Text("Loading...") // highlight-line
12        } // highlight-line
13    } // highlight-line
14}

Test pagination

Build and run the app, when you scroll to the bottom of the list you should see a row that says Tap to load more

Tap to load more

When you tap that row, the next set of launches will be fetched and loaded into the list. If you continue this process eventually the Tap to load more button will no longer be displayed because all launches have been loaded.

Tap to load more

Next, you'll complete the details view that will allow you to book a seat on a launch.