2. Complete the details view
13m

Overview

We've got a great overview of rocket , but we're missing the specifics: details about any particular .

In this lesson, we will:

  • Write a second to request details about a single
  • 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 in the LaunchList , and then pass that retrieved object on to the DetailViewController.

A diagram illustrating a single request for all the details for all the launches in the list

On the other hand, we could request the additional details for a single , when we need it, by passing that launch's identifier to a new .

A diagram illustrating a single request for one launch, using the launch's identifier to retrieve the relevant data

The first option (requesting all the details, for all the , 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 for exactly the data we need is one of the greatest benefits of using . 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 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 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.

A screenshot of an Xcode dialog window, showing that we have unchecked our project as a target for the GraphQL file

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

https://studio.apollographql.com/sandbox/explorer

Hovering over the button to open a new workspace in Explorer

A new tab will be added with nothing in it:

https://studio.apollographql.com/sandbox/explorer

The UI after opening a fresh, new workspace

In the left-hand Documentation panel, make sure that you can see the Query type with a list of available to choose from. (You might need to navigate back up to the root level.)

https://studio.apollographql.com/sandbox/explorer

An empty Operation workspace, with the Query type's fields highlighted

Select the launch by clicking the plus button next to it. will automatically set up the for you to use:

query Launch($launchId: ID!) {
launch(id: $launchId) {
}
}

First, change the name of the from "" to "LaunchDetails"—that will then reflect in the tab name and make it easier to tell which we're working with:

https://studio.apollographql.com/sandbox/explorer

The Operation tab now reflecting the name LaunchDetails

Let's go through what's been added here:

  • Again, we've added an , but this time it accepts a $launchId. This was added automatically by because $launchId is non-null and does not have a default value.
  • Looking more closely at $launchId, you'll notice that it is declared as an ID type. The ! annotation indicates that it is non-null.
  • Within the , we include the launch in the . launch accepts an id, which is set to the value of the $launchId.
  • Again, there's blank space for you to add the 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 is executed, the server will pass this as the value of $launchId.

Note: '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 , if you don't annotate a 's type with an !, that is considered nullable. This is because are nullable by default.

Keep this difference in mind when you switch between editing Swift and files.

Now in the , start by using the checkboxes or typing to add the same properties we're already requesting in the LaunchList . 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) {
id
site
mission {
name
missionPatch(size: LARGE)
}
}
}

Next, look in the left sidebar to see what other are available. Selecting rocket will add a set of brackets to request details about the 's rocket, and drill you into the rocket property, showing you the available on the Rocket type:

https://studio.apollographql.com/sandbox/explorer

Adding rocket to the query, and viewing available fields

Click the buttons to check off name and type. Next, go back to the Launch type by clicking launch from the Documentation panel breadcrumbs.

https://studio.apollographql.com/sandbox/explorer

Highlighting the launch option to return to the Launch type

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

query LaunchDetails($launchId: ID!) {
launch(id: $launchId) {
id
site
mission {
name
missionPatch(size: LARGE)
}
rocket {
name
type
}
isBooked
}
}

At the bottom of the Operation panel, update the Variables section to pass in an ID for a . In this case, it needs to be a string that contains a number:

Variables panel
{
"launchId": "25"
}

This tells to fill in the value of the $launchId with the value "25" when it runs the . Press the big play button, and you should get some results back for the with ID 25:

https://studio.apollographql.com/sandbox/explorer

Running the query for a particular launch's details and viewing the response

Now that we've confirmed it works, we'll copy the (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 .

Code generation command
./apollo-ios-cli generate

Execute the query

Now let's add the code to run this to retrieve our data.

Inside the main RocketReserver directory, go to DetailViewModel.swift and add this import:

DetailViewModel.swift
import SwiftUI
+ import RocketReserverAPI

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

DetailViewModel.swift
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 , so replace the TODO in the loadLaunchDetails method with this code:

DetailViewModel.swift
func loadLaunchDetails() {
guard launchID != launch?.id else {
return
}
Network.shared.apollo.fetch(query: LaunchDetailsQuery(launchId: launchID)) { [weak self] result in
guard 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 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:

DetailView.swift
import RocketReserverAPI
import SDWebImageSwiftUI

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

DetailView.swift
init(launchID: RocketReserverAPI.ID) {
_viewModel = StateObject(wrappedValue: DetailViewModel(launchID: launchID))
}

Almost done! Let's update the View , body, to use the data from DetailViewModel and call the loadLaunchDetails method:

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

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

LaunchListView.swift
ForEach(0..<viewModel.launches.count, id: \.self) { index in
NavigationLink(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 's id!

Let's jump back into our graphql folder, and open LaunchList.graphql. Here we can see that though we're for launches, we haven't included the oh-so-important id for each in the returned list. Let's update that now. Here's what your should look like.

query LaunchList {
launches {
cursor
hasMore
launches {
id
site
mission {
name
missionPatch(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 .

Test the DetailView

Now that everything is linked up, build and run the application. Now when you click on any you should see a corresponding DetailView like this:

A desktop view showing our IDE next to an simulator, which has navigated to a particular launch's page

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 with , including authentication.

Practice

Why is it generally preferable to create queries in child Views rather than passing data from their parent? Select all that apply.
Swift and GraphQL have different approaches to nullability. Select all the statements that are true.

Up next

Queries are only the beginning—now it's time to look at the that allow us to change data, !

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.