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 the DetailViewController.

  • 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:

the new tab button

A new tab will be added with nothing in it:

The UI after adding a new tab

In the left-hand column, click the word "Query" under "Documentation" to be brought to a list of possible queries:

The 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:

What the launch query will look like immediately after adding it

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:

The renamed query

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 the ID type here has an exclamation point, meaning it can't be null.

  • Within that operation, we make a call to the launch query. The id 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:

GraphQL
(Sandbox
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:

The available properties for Rocket

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:

The back button

Finally, check off the isBooked property on the Launch. Your final query should look like this:

GraphQL
(Sandbox
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:

JSON
(Sandbox
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:

Detail request returning JSON

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

Swift
DetailViewModel.swift
1import RocketReserverAPI

Next let's update the init() method and add some variables to hold our Launch data:

Swift
DetailViewModel.swift
1let launchID: RocketReserverAPI.ID // highlight-line
2
3@Published var launch: LaunchDetailsQuery.Data.Launch?// highlight-line
4@Published var isShowingLogin = false
5@Published var appAlert: AppAlert?
6
7init(launchID: RocketReserverAPI.ID) {// highlight-line
8    self.launchID = launchID// highlight-line
9}

Next we need to run the query, so replace the TODO in the loadLaunchDetails method with this code:

Swift
DetailViewModel.swift
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:

Swift
DetailView.swift
1import RocketReserverAPI
2import SDWebImageSwiftUI

Next, we need to update the init() method to initialize the DetailViewModel with a launchID:

Swift
DetailView.swift
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:

Swift
DetailView.swift
1var body: some View {
2    VStack {
3        if let launch = viewModel.launch { // highlight-line
4            HStack(spacing: 10) {
5                if let missionPatch = launch.mission?.missionPatch { // highlight-line 
6                    WebImage(url: URL(string: missionPatch)) // highlight-line
7                        .resizable() // highlight-line
8                        .placeholder(placeholderImg) // highlight-line
9                        .indicator(.activity) // highlight-line
10                        .scaledToFit() // highlight-line
11                        .frame(width: 165, height: 165) // highlight-line
12                } else { // highlight-line
13                    placeholderImg
14                        .resizable()
15                        .scaledToFit()
16                        .frame(width: 165, height: 165)
17                } // highlight-line
18
19                VStack(alignment: .leading, spacing: 4) {
20                    if let missionName = launch.mission?.name { // highlight-line
21                        Text(missionName)
22                            .font(.system(size: 24, weight: .bold))
23                    }
24
25                    if let rocketName = launch.rocket?.name { // highlight-line
26                        Text("🚀 \(rocketName)")
27                            .font(.system(size: 18))
28                    }
29
30                    if let launchSite = launch.site { // highlight-line
31                        Text(launchSite)
32                            .font(.system(size: 14))
33                    }
34                }
35
36                Spacer()
37            }
38
39            if launch.isBooked { // highlight-line
40                cancelTripButton()
41            } else {
42                bookTripButton()
43            }
44        }
45        Spacer()
46    }
47    .padding(10)
48    .navigationTitle(viewModel.launch?.mission?.name ?? "") // highlight-line
49    .navigationBarTitleDisplayMode(.inline)
50    .task {
51        viewModel.loadLaunchDetails() // highlight-line
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:

Swift
DetailView.swift
1struct DetailView_Previews: PreviewProvider {
2    static var previews: some View {
3        DetailView(launchID: "110") // highlight-line
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:

Swift
LaunchListView.swift
1ForEach(0..<viewModel.launches.count, id: \.self) { index in
2    NavigationLink(destination: DetailView(launchId: viewModel.launches[index].id)) { // highlight-line
3        LaunchRow(launch: viewModel.launches[index]) // highlight-line
4    } // highlight-line
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:

Completed DetailView

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 .