6. Complete the detail view
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 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, switch back to 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{ "launchId": "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. Build the application so that codegen picks up this new file and generates a new query type for it.
Now that you know what you're planning to ask for, it's time to set up the UI for the detail screen. Go to DetailViewController.swift
. First, add a place to hang on to the result of the query. Add the following property to the top of the class:
1private var launch: LaunchDetailsQuery.Data.Launch?
Next, update the viewDidLoad
function to clear out anything from the storyboard before attempting to configure the view:
1override func viewDidLoad() {
2 super.viewDidLoad()
3
4 self.missionNameLabel.text = "Loading..."
5 self.launchSiteLabel.text = nil
6 self.rocketNameLabel.text = nil
7 self.configureView()
8}
Delete the existing contents of configureView()
. In their place, start by adding a check that we have something to display, and a place to display it:
1guard
2 self.missionNameLabel != nil,
3 let launch = self.launch else {
4 return
5}
Next, it's time to display all the information you've gotten from your GraphQL server. Remember that GraphQL properties are nullable by default, so you'll often need to provide handling for when a given property is nil
.
Add the following code below the guard
statement you just added:
1self.missionNameLabel.text = launch.mission?.name
2self.title = launch.mission?.name
3
4let placeholder = UIImage(named: "placeholder")!
5
6if let missionPatch = launch.mission?.missionPatch {
7 self.missionPatchImageView.sd_setImage(with: URL(string: missionPatch)!, placeholderImage: placeholder)
8} else {
9 self.missionPatchImageView.image = placeholder
10}
11
12if let site = launch.site {
13 self.launchSiteLabel.text = "Launching from \(site)"
14} else {
15 self.launchSiteLabel.text = nil
16}
17
18if
19 let rocketName = launch.rocket?.name ,
20 let rocketType = launch.rocket?.type {
21 self.rocketNameLabel.text = "🚀 \(rocketName) (\(rocketType))"
22} else {
23 self.rocketNameLabel.text = nil
24}
25
26if launch.isBooked {
27 self.bookCancelButton.title = "Cancel trip"
28 self.bookCancelButton.tintColor = .red
29} else {
30 self.bookCancelButton.title = "Book now!"
31 // Get the color from the main window rather than the view to prevent alerts from draining color
32 self.bookCancelButton.tintColor = UIApplication.shared.windows.first?.tintColor
33}
Then, find the loadLaunchDetails()
method. Replace the TODO
with the following, which loads the details using the LaunchDetailsQuery
you created earlier:
1private func loadLaunchDetails() {
2 guard
3 let launchID = self.launchID,
4 launchID != self.launch?.id else {
5 // This is the launch we're already displaying, or the ID is nil.
6 return
7 }
8
9 Network.shared.apollo.fetch(query: LaunchDetailsQuery(launchId: launchID)) { [weak self] result in
10 guard let self = self else {
11 return
12 }
13
14 switch result {
15 case .failure(let error):
16 self.showAlert(title: "Network Error",
17 message: error.localizedDescription)
18 case .success(let graphQLResult):
19 if let launch = graphQLResult.data?.launch {
20 self.launch = launch
21 }
22
23 if let errors = graphQLResult.errors {
24 let message = errors
25 .map { $0.localizedDescription }
26 .joined(separator: "\n")
27 self.showAlert(title: "GraphQL Error(s)",
28 message: message)
29 }
30 }
31 }
32}
Finally, update the didSet
for launchID
to load the launch details if we don't already have them:
1var launchID: GraphQLID? {
2 didSet {
3 self.loadLaunchDetails()
4 }
5}
and add a didSet
on the launch
property to load the UI once the launch is actually loaded.
1private var launch: LaunchDetailsQuery.Data.Launch? {
2 didSet {
3 self.configureView()
4 }
5}
Build and run the application. When you tap into the detail screen, you should now see the full details:
You'll notice that many of the more recent launches have a rocket type of FT
. If you load more launches until you get to the end of the list, you'll get to some rockets that have different rocket types:
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.