Overview
We've got a great overview of rocket launches, but we're missing the specifics: details about any particular launch.
In this lesson, we will:
- Write a second GraphQL query to request details about a single launch
- Use that data in a
DetailView
Avoiding overfetching
To get more information to show on the detail page, we have a couple of options:
Our first option is to request all the details we want to display for every single launch in the LaunchList
query, and then pass that retrieved object on to the DetailViewController
.
On the other hand, we could request the additional details for a single launch, when we need it, by passing that launch's identifier to a new query.
The first option (requesting all the details, for all the launches, all at once) can seem easier if there isn't a substantial difference in size between the data we're requesting for the list, and the data we're requesting for each details page.
But it's important to remember that the ability to query for exactly the data we need is one of the greatest benefits of using GraphQL. If we don't actually need to display additional information, we can save bandwidth, execution time, and battery life by not asking for the data until we need it.
This is especially true when we have a much larger query for our detail view than for our list view. Passing the identifier and then fetching based on that is considered a best practice. Even though the amount of data in this case doesn't differ greatly, we'll opt for the second option—and construct a query that fetches details based on a provided launch ID.
Create the details query
Back in Xcode, create a new empty file (scroll down to find the Empty template under the Other section) in the graphql
folder and name it LaunchDetails.graphql
. Do not add it to the app target.
In this file, we'll add the details we want to display in the detail view. First, we'll return to Sandbox and make sure the query works! Or if you prefer, use the embedded instance below:
In the Explorer tab, start by clicking the plus button in the middle Operation panel:
A new tab will be added with nothing in it:
In the left-hand Documentation panel, make sure that you can see the Query
type with a list of available fields to choose from. (You might need to navigate back up to the root level.)
Select the launch
field by clicking the plus button next to it. Apollo Sandbox will automatically set up the query for you to use:
query Launch($launchId: ID!) {launch(id: $launchId) {}}
First, change the name of the operation from "Launch" to "LaunchDetails"—that will then reflect in the tab name and make it easier to tell which query we're working with:
Let's go through what's been added here:
- Again, we've added an operation, but this time it accepts a variable
$launchId
. This was added automatically by Apollo Sandbox because$launchId
is non-null and does not have a default value. - Looking more closely at
$launchId
, you'll notice that it is declared as anID
scalar type. The!
annotation indicates that it is non-null. - Within the query, we include the
launch
field in the selection set.launch
accepts an argumentid
, which is set to the value of the variable$launchId
. - Again, there's blank space for you to add the fields you want to get details for on the returned object, which in this case is a
Launch
. - Finally, at the bottom, the Variables section of the Operation panel has been expanded, and a dictionary has been added with a key of
"launchId"
. When the query is executed, the server will pass this as the value of$launchId
.
Note: GraphQL's assumptions about nullability are different from Swift's. In Swift, if you don't annotate a property's type with either a ?
or an !
, that property is non-nullable.
In GraphQL, if you don't annotate a field's type with an !
, that field is considered nullable. This is because GraphQL fields are nullable by default.
Keep this difference in mind when you switch between editing Swift and GraphQL files.
Now in the Apollo Sandbox, start by using the checkboxes or typing to add the same properties we're already requesting in the LaunchList
query. One difference: Use LARGE
for the mission patch size since the patch will be displayed in a much larger ImageView
:
query LaunchDetails($launchId: ID!) {launch(id: $launchId) {idsitemission {namemissionPatch(size: LARGE)}}}
Next, look in the left sidebar to see what other fields are available. Selecting rocket
will add a set of brackets to request details about the launch's rocket, and drill you into the rocket
property, showing you the available fields on the Rocket
type:
Click the buttons to check off name
and type
. Next, go back to the Launch
type by clicking launch
from the Documentation panel breadcrumbs.
Finally, check off the isBooked
property on the Launch
. Your final query should look like this:
query LaunchDetails($launchId: ID!) {launch(id: $launchId) {idsitemission {namemissionPatch(size: LARGE)}rocket {nametype}isBooked}}
At the bottom of the Operation panel, update the Variables section to pass in an ID for a launch. In this case, it needs to be a string that contains a number:
{"launchId": "25"}
This tells Apollo Sandbox to fill in the value of the $launchId
variable with the value "25"
when it runs the query. Press the big play button, and you should get some results back for the launch with ID 25:
Now that we've confirmed it works, we'll copy the query (either by selecting all the text or using the Copy Operation option from the meatball menu as before) and paste it into our LaunchDetails.graphql
file. Run the code generation from the terminal to generate the code for the new query.
./apollo-ios-cli generate
Execute the query
Now let's add the code to run this query to retrieve our data.
Inside the main RocketReserver
directory, go to DetailViewModel.swift
and add this import
:
import SwiftUI+ import RocketReserverAPI
Next let's update the init()
method and add some variables to hold our launch data:
class DetailViewModel: ObservableObject {let launchID: RocketReserverAPI.ID@Published var launch: LaunchDetailsQuery.Data.Launch?@Published var isShowingLogin = false@Published var appAlert: AppAlert?init(launchID: RocketReserverAPI.ID) {self.launchID = launchID}// ...other methods}
Next we need to run the query, so replace the TODO
in the loadLaunchDetails
method with this code:
func loadLaunchDetails() {guard launchID != launch?.id else {return}Network.shared.apollo.fetch(query: LaunchDetailsQuery(launchId: launchID)) { [weak self] result inguard let self = self else {return}switch result {case .success(let graphQLResult):if let launch = graphQLResult.data?.launch {self.launch = launch}if let errors = graphQLResult.errors {self.appAlert = .errors(errors: errors)}case .failure(let error):self.appAlert = .errors(errors: [error])}}}
Now that we have our query executing, we need to update the UI code to use the new data.
Update UI code
To start, go to DetailView.swift
and add the following import
statements:
import RocketReserverAPIimport SDWebImageSwiftUI
Next, we need to update the init()
method to initialize the DetailViewModel
with a launchID
:
init(launchID: RocketReserverAPI.ID) {_viewModel = StateObject(wrappedValue: DetailViewModel(launchID: launchID))}
Almost done! Let's update the View
variable, body
, to use the launch data from DetailViewModel
and call the loadLaunchDetails
method:
var body: some View {VStack {if let launch = viewModel.launch {HStack(spacing: 10) {if let missionPatch = launch.mission?.missionPatch {WebImage(url: URL(string: missionPatch)).resizable().placeholder(placeholderImg).indicator(.activity).scaledToFit().frame(width: 165, height: 165)} else {placeholderImg.resizable().scaledToFit().frame(width: 165, height: 165)}VStack(alignment: .leading, spacing: 4) {if let missionName = launch.mission?.name {Text(missionName).font(.system(size: 24, weight: .bold))}if let rocketName = launch.rocket?.name {Text("🚀 \(rocketName)").font(.system(size: 18))}if let launchSite = launch.site {Text(launchSite).font(.system(size: 14))}}Spacer()}if launch.isBooked {cancelTripButton()} else {bookTripButton()}}Spacer()}.padding(10).navigationTitle(viewModel.launch?.mission?.name ?? "").navigationBarTitleDisplayMode(.inline).task {viewModel.loadLaunchDetails()}.sheet(isPresented: $viewModel.isShowingLogin) {LoginView(isPresented: $viewModel.isShowingLogin)}.appAlert($viewModel.appAlert)}
One more change to make before we leave this file: scroll down to find DetailView_Previews
. Here, we'll need to update the preview code to the following:
struct DetailView_Previews: PreviewProvider {static var previews: some View {DetailView(launchID: "110")}}
Now we just need to connect the DetailView
to our LaunchListView
. So let's go to LaunchListView.swift
and update our List
to the following:
ForEach(0..<viewModel.launches.count, id: \.self) { index inNavigationLink(destination: DetailView(launchID: viewModel.launches[index].id)) {LaunchRow(launch: viewModel.launches[index])}}
But wait - there's a problem here! Right about now, we should be seeing an error appear above the line that we just finished writing.
Value of type 'LaunchListQuery.Data.Launches.Launch' has no member 'id'
Fixing the query
We're trying to make a link out of each of our rows, but it turns out that the data we receive as part of our LaunchListQuery
actually doesn't contain each launch's id!
Let's jump back into our graphql
folder, and open LaunchList.graphql
. Here we can see that though we're querying for launches
, we haven't included the oh-so-important id
field for each launch in the returned list. Let's update that now. Here's what your query should look like.
query LaunchList {launches {cursorhasMorelaunches {idsitemission {namemissionPatch(size: SMALL)}}}}
Back in your terminal (make sure it's opened to the starter
directory), run the command to generate our code down in the RocketReserverAPI
package.
./apollo-ios-cli generate
This regenerates our LaunchListQuery.graphql
file—and with any luck, that pesky error in LaunchListView
will disappear momentarily!
Now our view should be working: this change will allow us to click on any LaunchRow
in our list and load the DetailView
for that launch.
Test the DetailView
Now that everything is linked up, build and run the application. Now when you click on any launch you should see a corresponding DetailView
like this:
You may have noticed that the detail view includes a Book Now!
button, but there's no way to book a seat yet. To fix that, let's learn how to make changes to objects in your graph with mutations, including authentication.
Practice
Up next
Queries are only the beginning—now it's time to look at the operations that allow us to change data, mutations!
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.