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
LaunchList
query, 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
launchId
argument.The parameter is prefixed with a
$
for its name, and the type is indicated immediately after. Note that theID
type here has an exclamation point, meaning it can't be null.Within that operation, we make a call to the
launch
query. Theid
is the argument the query is expecting, and the$launchId
is 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$launchId
parameter.
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 this import
1import RocketReserverAPI
Next 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() {
2 guard launchID != launch?.id else {
3 return
4 }
5
6 Network.shared.apollo.fetch(query: LaunchDetailsQuery(launchId: launchID)) { [weak self] result in
7 guard let self = self else {
8 return
9 }
10
11 switch result {
12 case .success(let graphQLResult):
13 if let launch = graphQLResult.data?.launch {
14 self.launch = launch
15 }
16
17 if let errors = graphQLResult.errors {
18 self.appAlert = .errors(errors: errors)
19 }
20 case .failure(let error):
21 self.appAlert = .errors(errors: [error])
22 }
23 }
24}
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 SDWebImageSwiftUI
Next, 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))
7 .resizable()
8 .placeholder(placeholderImg)
9 .indicator(.activity)
10 .scaledToFit()
11 .frame(width: 165, height: 165)
12 } else {
13 placeholderImg
14 .resizable()
15 .scaledToFit()
16 .frame(width: 165, height: 165)
17 }
18
19 VStack(alignment: .leading, spacing: 4) {
20 if let missionName = launch.mission?.name {
21 Text(missionName)
22 .font(.system(size: 24, weight: .bold))
23 }
24
25 if let rocketName = launch.rocket?.name {
26 Text("🚀 \(rocketName)")
27 .font(.system(size: 18))
28 }
29
30 if let launchSite = launch.site {
31 Text(launchSite)
32 .font(.system(size: 14))
33 }
34 }
35
36 Spacer()
37 }
38
39 if launch.isBooked {
40 cancelTripButton()
41 } else {
42 bookTripButton()
43 }
44 }
45 Spacer()
46 }
47 .padding(10)
48 .navigationTitle(viewModel.launch?.mission?.name ?? "")
49 .navigationBarTitleDisplayMode(.inline)
50 .task {
51 viewModel.loadLaunchDetails()
52 }
53 .sheet(isPresented: $viewModel.isShowingLogin) {
54 LoginView(isPresented: $viewModel.isShowingLogin)
55 }
56 .appAlert($viewModel.appAlert)
57}
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.