w3resource

Connect your queries to your UI


In the previous tutorial, we executed queries against a GraphQL server. In this tutorial, we will reflect the results of those queries in our UI.

Remove boilerplate code

In MasterViewController.swift, delete the following pieces of boilerplate code that the app doesn't need:

  1. The objects property
  2. Everything in viewDidLoad except the super.viewDidLoad() call
  3. The entire insertNewObject() method
  4. The contents of prepare(for segue:) (but not the method itself)
  5. The entire tableView(_, canEditRowAt:) method
  6. The entire tableView(_, commit:, forRowAt:) method

Add query fields

Now let's add properties to display the results of the LaunchListQuery you built in the previous tutorial step.

At the top of MasterViewController.swift, add a new property to store the launches that the query returns:

var launches = [LaunchListQuery.Data.Launch.Launch]()

Why the long name? Each query returns its own nested object structure to ensure that when you use the result of a particular query, you can't ask for a property that isn't present. Because this screen will be populated by the results of the LaunchListQuery, you need to display subtypes of that particular query.

Next, add an enum that helps handle dealing with sections (we'll add items to the enum later):

enum ListSection: Int, CaseIterable {
  case launches
}

Fill in required methods

Now we can update the various UITableViewDataSource methods to use the result of our query.

For numberOfSections(in:), you can use the allCases property from CaseIterable to provide the appropriate number of sections:

override func numberOfSections(in tableView: UITableView) -> Int {
  return ListSection.allCases.count
}

For tableView(_:numberOfRowsInSection:), you can try instantiating a ListSection enum object. If it doesn't work, that's an invalid section, and if it does, you can switch directly on the result. In this case, you'll want to return the count of launches:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  guard let listSection = ListSection(rawValue: section) else {
    assertionFailure("Invalid section")
    return 0
  }
        
  switch listSection {
  case .launches:
    return self.launches.count
  }
}

For tableView(_:cellForRowAt:), you can use the existing cell dequeueing mechansim, the same section check as in tableView(_:numberOfRowsInSection), and then configure the cell based on what section it's in.

For this initial section, grab a launch out of the launches array at the index of indexPath.row, and update the textLabel to display the launch site:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

  guard let listSection = ListSection(rawValue: indexPath.section) else {
    assertionFailure("Invalid section")
    return cell
  }
    
  switch listSection {
  case .launches:
    let launch = self.launches[indexPath.row]
    cell.textLabel?.text = launch.site
  }
    
  return cell
}

Your table view has all the information it needs to populate itself when the launches array has contents. Now it's time to actually get those contents from the server.

First, add a method for showing errors at the end of your file:

private func showErrorAlert(title: String, message: String) {
  let alert = UIAlertController(title: title,
                                message: message,
                                preferredStyle: .alert) 
  alert.addAction(UIAlertAction(title: "OK", style: .default))
  self.present(alert, animated: true)
}

Next, add a method to load the launches. You'll use a setup similar to the one you used to set this up in the AppDelegate earlier.

However, you need to make sure that a call doesn't try to call back and use elements that are no longer there, so you'll check to make sure that the MasterViewController hasn't been deallocated out from under you by passing in [weak self] and unwrapping self before proceeding with updating the UI:

private func loadLaunches() {
  Network.shared.apollo
    .fetch(query: LaunchListQuery()) { [weak self] result in
    
      guard let self = self else {
        return
      }
      defer {
        self.tableView.reloadData()
      }     
      switch result {
      case .success(let graphQLResult):
        // TODO
      case .failure(let error):
        self.showErrorAlert(title: "Network Error",
                            message: error.localizedDescription)
      }
  }
}

GraphQLResult has both a data property and an errors property. This is because GraphQL allows partial data to be returned if it's non-null.

In the example we're working with now, we could theoretically obtain a list of launches, and then an error stating that a launch with a particular ID could not be constructed.

This is why when you get a GraphQLResult, you generally want to check both the data property (to display any results you got from the server) and the errors property (to try to handle any errors you received from the server).

Replace the // TODO in the code above with the following code to handle both data and errors:

if let launchConnection = graphQLResult.data?.launches {
  self.launches.append(contentsOf: launchConnection.launches.compactMap { $0 })
}
        
if let errors = graphQLResult.errors {
  let message = errors
        .map { $0.localizedDescription }
        .joined(separator: "\n")
  self.showErrorAlert(title: "GraphQL Error(s)",
                      message: message)    
}

Finally, you need to actually call the method you just added to kick off the call to the network when the view is first loaded. Update your viewDidLoad to also call loadLaunches:

override func viewDidLoad() {
  super.viewDidLoad()
  self.loadLaunches()
}

Build and run the application. After the query completes, a list of launch sites appears:

apollo graphql: connect your queries to your ui image

However, if you attempt to tap one of the rows, the app displays the detail with the placeholder text you can see in the storyboard, instead of any actual information about the launch:

apollo graphql: connect your queries to your ui image

To send that information through, you need to build out the MasterViewController's prepareForSegue method, and have a way for that method to pass the DetailViewController information about the launch.

Pass information to the detail view

Let's update the DetailViewController to be able to handle information about a launch.

Open DetailViewController.swift and delete the detailItem property at the bottom of the class (you won't need it).

Next, because you're going to add a property that uses a type defined in the Apollo SDK, you need to add an import statement to the top of the file:

import Apollo

Next, add a new property at the top of the class:

var launchID: GraphQLID? {
  didSet {
    self.configureView()
  }
}

This settable property allows the MasterViewController to pass along the identifier for the selected launch. The identifier will be used later to load more details about the launch.

For now, update the configureView() method to use this new property (if it's there) instead of the detailItem property you just deleted:

func configureView() {
  // Update the user interface for the detail item.
  guard
    let label = self.detailDescriptionLabel,
    let id = self.launchID else {
      return
  }

  label.text = "Launch \(id)"
}

Note: You're also unwrapping the detailDescriptionLabel because even though it's an Implicitly Unwrapped Optional, it won't be present if configureView is called before viewDidLoad.

Next, back in MasterViewController.swift, update the prepareForSegue method to obtain the most recently selected row and pass its corresponding launch details to the detail view controller:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  guard let selectedIndexPath = self.tableView.indexPathForSelectedRow else {
    // Nothing is selected, nothing to do
    return
  }
    
  guard let listSection = ListSection(rawValue: selectedIndexPath.section) else {
    assertionFailure("Invalid section")
    return
  }
    
  switch listSection {
  case .launches:
    guard
      let destination = segue.destination as? UINavigationController,
      let detail = destination.topViewController as? DetailViewController else {
        assertionFailure("Wrong kind of destination")
        return
    }
    
    let launch = self.launches[selectedIndexPath.row]
    detail.launchID = launch.id
    self.detailViewController = detail
  }
}

One slightly unrelated detail: In SceneDelegate.swift, one line relies on the detailItem property you deleted earlier, which now produces an error. Replace the erroring line with:

if topAsDetailController.launchID == nil {

Build and run, and tap on any of the launches. You'll now see the launch ID for the selected launch when you land on the page

Previous: Complete the detail view
Next: Create your project



Become a Patron!

Follow us on Facebook and Twitter for latest update.

It will be nice if you may share this link in any developer community or anywhere else, from where other developers may find this content. Thanks.

https://www.w3resource.com/apollo-graphql/connect-your-queries-to-your-ui.php