5. Define additional mutations
13m

Overview

We're just a couple steps away from booking and cancelling trips—all that's missing are a few new !

In this lesson, we will:

  • Implement the BookTrip and CancelTrip s
  • Complete the views to enable these features in our app

Add the BookTrip mutation

Back in Sandbox, let's take a closer look at the following bookTrips :

mutation BookTripsMutation($bookTripsLaunchIds: [ID]!) {
bookTrips(launchIds: $bookTripsLaunchIds) {
}
}

In the left sidebar (Documentation), click Root -> Mutation -> bookTrips. You can see that this declares a $bookTripsLaunchIds which is a list of IDs. The output object defines three :

  • A success boolean indicating whether the booking succeeded
  • A message string to display to the user
  • A list of launches the current user has booked

Click the plus signs next to success and message to add those to the .

In the Variables section of , add an array of identifiers. In this case, we'll use a single identifier to book one trip:

Variables
{
"bookTripsLaunchIds": ["25"]
}

Next, directly next to the word Variables, you'll see the word Headers. Click that to bring up the Headers section. Click the New Header button, and add Authorization in the header key text box.

https://studio.apollographql.com/sandbox/explorer

Explorer with the Headers panel open, highlighting the Authorization header that has been added

For the value, we'll paste the token we got back in the last section:

Authorization token
bWVAZXhhbXBsZS5jb20=

Now, let's fire off the . We'll get back information regarding the trips (or in this case, trip) we've just booked.

Note: If you receive an error that says "Cannot read property 'id' of null", that means your user was not found based on the token you passed through. Make sure your authorization header is properly formatted and that you're actually logged in!

https://studio.apollographql.com/sandbox/explorer

Explorer showing that the trip was booked successfully

With a written like this, we can book any number of trips we want at the same time. However, the booking mechanism in our application will only let us book one trip at a time.

Luckily, there's an easy way to update the so it's required to only accept a single object. First, update the name of your in Explorer to the singular BookTrip. Next, update the to take a single $id, then pass an array containing that $id to the bookTrips :

mutation BookTrip($id: ID!) {
bookTrips(launchIds: [$id]) {
success
message
}
}

This is helpful because the Swift code generation will now generate a method that only accepts a single ID instead of an array, but you'll still be calling the same under the hood, without the backend needing to change anything.

In the Variables section of , update the JSON dictionary to use id as the key, and remove the array brackets from around the identifier:

Variables
{
"id": "25"
}

Run the updated . The response we get back should be identical to the one we got earlier:

https://studio.apollographql.com/sandbox/explorer

The result of booking a trip with a single identifier

Now that we've fleshed out our , it's time to put it into the app. Go to File > New > File... > Empty, name this file BookTrip.graphql and add it next to the other files. (Make sure you don't add it to the app target.)

Paste in the final from the .

BookTrip.graphql
mutation BookTrip($id: ID!) {
bookTrips(launchIds: [$id]) {
success
message
}
}

Now run code generation in Terminal to generate the new code.

Code generation command
./apollo-ios-cli generate

We should see a new file was added to our RocketReserverAPI local package, under Mutations: BookTripMutation.graphql!

Implement the bookTrip logic

Now that we have the BookTrip , it's time to implement the logic to book a trip in the app.

Start by going to DetailViewModel.swift and finding the bookTrip() method.

Replace the function with the following code:

DetailViewModel.swift
private func bookTrip(with id: RocketReserverAPI.ID) {
Network.shared.apollo.perform(mutation: BookTripMutation(id: id)) { [weak self] result in
guard let self = self else {
return
}
switch result {
case .success(let graphQLResult):
if let bookingResult = graphQLResult.data?.bookTrips {
if bookingResult.success {
self.appAlert = .basic(title: "Success!",
message: bookingResult.message ?? "Trip booked successfully")
self.loadLaunchDetails()
} else {
self.appAlert = .basic(title: "Could not book trip",
message: bookingResult.message ?? "Unknown failure")
}
}
if let errors = graphQLResult.errors {
self.appAlert = .errors(errors: errors)
}
case .failure(let error):
self.appAlert = .errors(errors: [error])
}
}
}
Task!

Before we run it, let's add the code to cancel a trip as well.

Add the CancelTrip mutation

The process for the CancelTrip is similar to the one for BookTrip. Here's how it looks in :

mutation CancelTrip($id: ID!) {
cancelTrip(launchId: $id) {
success
message
}
}

One key difference from bookTrips is that we're only allowed to cancel one trip at a time because only one ID! is accepted as a parameter.

In the Variables section of , we can use the exact same JSON that we used for BookTrip (because it also used a single identifier called "id"):

Variables
{
"id": "25"
}

Important: Make sure that in the Headers section, you add your authorization token again (the token added to the tab with BookTrip won't carry over to this new tab).

Run the to cancel the trip, and we should see a successful response!

https://studio.apollographql.com/sandbox/explorer

The result of cancelling a trip

Implement the cancelTrip logic

Once again, go back to Xcode and create a new empty file (don't add it to the app target), name it CancelTrip.graphql, and add it next to your other files. Then, paste in the final from :

CancelTrip.graphql
mutation CancelTrip($id: ID!) {
cancelTrip(launchId: $id) {
success
message
}
}

Now run code generation in Terminal to generate the new code.

Code generation command
./apollo-ios-cli generate

Now let's implement the logic to cancel a trip in the app. Go back to DetailViewModel.swift, find the cancelTrip() method and replace it with the following code:

DetailViewModel.swift
private func cancelTrip(with id: RocketReserverAPI.ID) {
Network.shared.apollo.perform(mutation: CancelTripMutation(id: id)) { [weak self] result in
guard let self = self else {
return
}
switch result {
case .success(let graphQLResult):
if let cancelResult = graphQLResult.data?.cancelTrip {
if cancelResult.success {
self.appAlert = .basic(title: "Trip cancelled",
message: cancelResult.message ?? "Your trip has been officially cancelled")
self.loadLaunchDetails()
} else {
self.appAlert = .basic(title: "Could not cancel trip",
message: cancelResult.message ?? "Unknown failure.")
}
}
if let errors = graphQLResult.errors {
self.appAlert = .errors(errors: errors)
}
case .failure(let error):
self.appAlert = .errors(errors: [error])
}
}
}

One more thing we need to do is update the bookOrCancel() method to actually call our bookTrip(...) and cancelTrip(...) methods, replace the TODO in bookOrCancel() with the following code:

DetailViewModel.swift
guard let launch = launch else {
return
}
launch.isBooked ? cancelTrip(with: launch.id) : bookTrip(with: launch.id)
Task!

Testing our changes

Now build and run the application. If we go to the detail view for any and click Book trip we should get a message that the trip was successfully booked—but you'll notice that the UI doesn't update, even if we go out of the detail view and back into it again.

A screenshot of the IDE and simulator showing that a trip was booked successfully

Why is that? Well, this particular trip—where it's stored locally in the cache—still has the old value for isBooked.

There are a few ways to fix this, but for now we'll focus on the one that requires the fewest changes to the code: re-fetching the booking info from the network.

Force a fetch from the network

The fetch method of ApolloClient provides defaults for most of its parameters, so if you're using the default configuration, the only value we need to provide is the Query.

However, an important parameter to be aware of is the cachePolicy. By default, this has the value of returnCacheDataElseFetch, which does essentially what it says on the label:

  1. It first looks in the current cache (by default an in-memory cache) for data...
  2. ...and it fetches it from the network if it's not present in the cache!

If the data is present, the default behavior is to return the local copy to prevent an unnecessary network fetch. However, this is sometimes not the desired behavior (especially after executing a ).

There are several different cache policies available to you, but the easiest way to absolutely force a refresh from the network that still updates the cache is to use fetchIgnoringCacheData. This policy bypasses the cache when going to the network, but it also stores the results of the fetch in the cache for future use.

First, we need to add the following import to DetailViewModel:

DetailViewModel.swift
import SwiftUI
import RocketReserverAPI
import KeychainSwift
+ import Apollo

Update the loadLaunchDetails method to take a parameter to determine if it should force reload. If it should force reload, update the cache policy from the default .returnCacheDataElseFetch, which will return data from the cache if it exists, to .fetchIgnoringCacheData:

DetailViewModel.swift
func loadLaunchDetails(forceReload: Bool = false) {
guard forceReload || launchID != launch?.id else {
return
}
let cachePolicy: CachePolicy = forceReload ? .fetchIgnoringCacheData : .returnCacheDataElseFetch
Network.shared.apollo.fetch(query: LaunchDetailsQuery(launchId: launchID), cachePolicy: cachePolicy) { [weak self] result in
// Network handling remains the same
}
}

Next, update both the bookTrip(...) and cancelTrip(...) methods to use the updated loadLaunchDetails(...) call:

DetailViewModel.swift
// bookTrip()
self.appAlert = .basic(title: "Success!",
message: bookingResult.message ?? "Trip booked successfully")
self.loadLaunchDetails(forceReload: true)
// cancelTrip()
self.appAlert = .basic(title: "Trip cancelled",
message: cancelResult.message ?? "Your trip has been officially cancelled")
self.loadLaunchDetails(forceReload: true)

Test the mutations

Run the application. Now when we book or cancel a trip, the application will fetch the updated state and, accordingly, update the UI with the correct state. When we go out and back in, the cache will be updated with the most recent state, and the most recent state will display.

A screenshot of the IDE and simulator showing that our booking state is reflected in the UI

Task!

Up next

Coming up next, let's explore how we can show different subsets of our data using pagination.

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.