9. Complete the details view
In this section, you'll write a second GraphQL query that requests details about a single launch and uses that data in a DetailView
To get more information to show on the detail page, you have a couple of options:
You could request all the details you want to display for every single launch in the
LaunchListquery, and then pass that retrieved object on to theDetailViewController.You could provide the identifier of an individual launch to a different query to request all the details you want to display.
The first option can seem easier if there isn't a substantial difference in size between what you're requesting for the list versus the detail page.
However, remember that one of the advantages of GraphQL is that you can query for exactly the data you need to display on a page. If you're not going to be displaying additional information, you can save bandwidth, execution time, and battery life by not asking for data until you need it.
This is especially true when you have a much larger query for your detail view than for your 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, you'll build out a query to help fetch details based on the ID so you'll know how to do it in the future.
Create the details query
Create a new empty file and name it LaunchDetails.graphql. In this file, you'll add the details you want to display in the detail view. First, you'll want to go back to your Sandbox and make sure that your query works!
In the Explorer tab, start by clicking the "New Tab" button in the middle operations section:
A new tab will be added with nothing in it:
In the left-hand column, click the word "Query" under "Documentation" to be brought to a list of possible queries:
Select the launch query by clicking the button next to it. Sandbox Explorer will automatically set up the query for you to use:
First, change the name of the operation from "Query" to "LaunchDetails" - that will then reflect in the tab name and make it easier to tell which query you're working with:
Let's go through what's been added here:
Again, we've added an operation, but this time it's got a parameter coming into it. This was added automatically by Sandbox Explorer because there is not a default value provided for the non-null
launchIdargument.The parameter is prefixed with a
$for its name, and the type is indicated immediately after. Note that theIDtype here has an exclamation point, meaning it can't be null.Within that operation, we make a call to the
launchquery. Theidis the argument the query is expecting, and the$launchIdis the name of the parameter we just passed in the line above.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 Operations panel has been expanded, and a dictionary has been added with a key of
"launchId". At runtime, this will be used to fill in the blank of the$launchIdparameter.
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 question mark or an exclamation point, that property is non-nullable.
In GraphQL, if you don't annotate a field's type with an exclamation point, 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 Sandbox Explorer, start by using the checkboxes or typing to add the properties you'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:
1query LaunchDetails($id:ID!) { 2 launch(id: $id) { 3 id 4 site 5 mission { 6 name 7 missionPatch(size:LARGE) 8 } 9 } 10}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 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 Launch by clicking the back button next to the Rocket type in the left sidebar:
Finally, check off the isBooked property on the Launch. Your final query should look like this:
1query LaunchDetails($launchId: ID!) { 2 launch(id: $launchId) { 3 id 4 site 5 mission { 6 name 7 missionPatch(size: LARGE) 8 } 9 rocket { 10 name 11 type 12 } 13 isBooked 14 } 15}At the bottom of the Operations section, 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:
1{ "id": "25" }This tells Sandbox Explorer 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 you've confirmed it worked, 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 your LaunchDetails.graphql file. Run the code generation from Terminal to generate the code for the new query.
Execute the query
Now let's add the code to run this query to retrieve our data.
Go to DetailViewModel.swift and add the following import statements:
1import Apollo 2import RocketReserverAPINext let's update the init() method and add some variables to hold our Launch data:
1let launchID: RocketReserverAPI.ID 2 3@Published var launch: LaunchDetailsQuery.Data.Launch? 4@Published var isShowingLogin = false 5@Published var appAlert: AppAlert? 6 7init(launchID: RocketReserverAPI.ID) { 8 self.launchID = launchID 9}Next we need to run the query, so replace the TODO in the loadLaunchDetails method with this code:
1func loadLaunchDetails() async { 2 guard launchID != launch?.id else { 3 return 4 } 5 6 do { 7 let response = try await ApolloClient.shared.fetch( 8 query: LaunchDetailsQuery(launchId: launchID) 9 ) 10 11 if let errors = response.errors { 12 appAlert = .errors(errors: errors) 13 } 14 15 if let launch = response.data?.launch { 16 self.launch = launch 17 } 18 } catch { 19 appAlert = .errors(errors: [error]) 20 } 21}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:
1import RocketReserverAPI 2import SDWebImageSwiftUINext, we need to update the init() method to initialize the DetailViewModel with a launchID:
1init(launchID: RocketReserverAPI.ID) { 2 _viewModel = StateObject(wrappedValue: DetailViewModel(launchID: launchID)) 3}Almost done! Let's update the body View variable to use the launch data from DetailViewModel and call the loadLaunchDetails method:
1var body: some View { 2 VStack { 3 if let launch = viewModel.launch { 4 HStack(spacing: 10) { 5 if let missionPatch = launch.mission?.missionPatch { 6 WebImage(url: URL(string: missionPatch)) { image in 7 image.resizable() 8 } placeholder: { 9 placeholderImg.resizable() 10 } 11 .indicator(.activity) 12 .scaledToFit() 13 .frame(width: 165, height: 165) 14 } else { 15 placeholderImg 16 .resizable() 17 .scaledToFit() 18 .frame(width: 165, height: 165) 19 } 20 21 VStack(alignment: .leading, spacing: 4) { 22 if let missionName = launch.mission?.name { 23 Text(missionName) 24 .font(.system(size: 24, weight: .bold)) 25 } 26 27 if let rocketName = launch.rocket?.name { 28 Text("🚀 \(rocketName)") 29 .font(.system(size: 18)) 30 } 31 32 if let launchSite = launch.site { 33 Text(launchSite) 34 .font(.system(size: 14)) 35 } 36 } 37 38 Spacer() 39 } 40 41 if launch.isBooked { 42 cancelTripButton() 43 } else { 44 bookTripButton() 45 } 46 } 47 Spacer() 48 } 49 .padding(10) 50 .navigationTitle(viewModel.launch?.mission?.name ?? "") 51 .navigationBarTitleDisplayMode(.inline) 52 .task { 53 await viewModel.loadLaunchDetails() 54 } 55 .sheet(isPresented: $viewModel.isShowingLogin) { 56 LoginView(isPresented: $viewModel.isShowingLogin) 57 } 58 .appAlert($viewModel.appAlert) 59}Also, you'll need to update the preview code in DetailView to this:
1struct DetailView_Previews: PreviewProvider { 2 static var previews: some View { 3 DetailView(launchID: "110") 4 } 5}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:
1ForEach(0..<viewModel.launches.count, id: \.self) { index in 2 NavigationLink(destination: DetailView(launchId: viewModel.launches[index].id)) { 3 LaunchRow(launch: viewModel.launches[index]) 4 } 5}This 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 and 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.