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$launchIdis non-null and does not have a default value. - Looking more closely at
$launchId, you'll notice that it is declared as anIDscalar type. The!annotation indicates that it is non-null. - Within the query, we include the
launchfield in the selection set.launchaccepts 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.