SwiftUI and Structured Concurrency

Learn how to manage concurrency into your SwiftUI iOS app. By Andrew Tetlaw.

5 (6) · 4 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Fetching All Rovers

To gain a little more control over your child tasks, you can use a TaskGroup. In the Model folder, create a file called MarsData.swift and replace the default code with this:

import SwiftUI
import OSLog

class MarsData {
  let marsRoverAPI = MarsRoverAPI()
}

You’ll use this class to encapsulate all the tasks the views need to perform. It contains a reference to the MarsRoverAPI so you can make API calls.

Until now, you’ve manually specified each rover’s name, but an API is available to obtain all current rovers. After all, what if NASA launches a new one?

Add the following to MarsData:

func fetchAllRovers() async -> [Rover] {
  do {
    return try await marsRoverAPI.allRovers()
  } catch {
    log.error("Error fetching rovers: \(String(describing: error))")
    return []
  }
}

This calls the API endpoint that returns an array of Rover. You’ll use this shortly.

Creating a Task Group

Next, you’ll create a function that returns the latest photos similar to the code you’ve already written, but within a TaskGroup. Begin the function like this in MarsData:

// 1
func fetchLatestPhotos() async -> [Photo] {
  // 2
  await withTaskGroup(of: Photo?.self) { group in
  }
}

Here’s a code breakdown:

  1. Your new async function will return an array of Photo.
  2. You use withTaskGroup(of:returning:body:) to create the task group and specify that each task in the group will return an optional Photo.

To fetch the list of rovers, add this to the closure:

let rovers = await fetchAllRovers()

rovers will hold your rover list and let you add a task for each. Dynamically adding tasks only when needed is one of the significant benefits of using a TaskGroup.

Continue by adding the following:

// 1
for rover in rovers {
  // 2
  group.addTask {
    // 3
    let photos = try? await self.marsRoverAPI.latestPhotos(rover: rover)
    return photos?.first
  }
}

Here, you:

  1. Loop through each rover.
  2. Call addTask(priority:operation:) on the group and pass a closure representing the work the task needs to perform.
  3. In the task, you call latestPhotos(rover:) and then return the first photo if any were retrieved, or nil if none were found.

You added tasks to the group, so now you need to collect the results. A TaskGroup also conforms to AsyncSequence for all the task results. All you need to do is loop through the tasks and collect the results. Add the following below your previous code:

// 1
var latestPhotos: [Photo] = []
// 2
for await result in group {
  // 3
  if let photo = result {
    latestPhotos.append(photo)
  }
}
// 4
return latestPhotos

Here’s what this does:

  1. First you set up an empty array to hold the returned photos.
  2. Then you create a for loop that loops through the asynchronous sequence provided by the group, waiting for each value from the child task.
  3. If the photo isn’t nil, you append it to the latestPhotos array.
  4. Finally, return the array.

Return to LatestView.swift and add a new property to LatestView:

let marsData = MarsData()

Replace the entire do-catch block from .task with:

latestPhotos = await marsData.fetchLatestPhotos()

This sets the latestPhotos state just like your previous code.

Build and run your app.

A Mars landscape photo by Perseverance

The latest photos will load as before. Now you’re prepared for when NASA launches its next Mars rover.

You won’t have to update your app. It’ll work like magic!

or adding a group task with:

This was a short-lived version of the API during the Xcode 13 beta. You’ll even find it in Apple presentations from WWDC 2021.

Be aware that it’s deprecated, and Xcode will display warnings.

Note: If you’re reading tutorials or looking through some Apple sample code you may see a different syntax. For example creating a task with:
async { 
  // ... 
}
group.async { 
  //... 
}
async { 
  // ... 
}
group.async { 
  //... 
}

Exploring All Photos

Because there are so many Mars rover photos available, give your users a way to see them all. The second tab in the app displays a RoversListView, where you’ll list all the available rovers and show how many photos are available to browse. To do this, you’ll first need to download a photo manifest for each rover.

In MarsData.swift, add the following function to MarsData:

// 1
func fetchPhotoManifests() async throws -> [PhotoManifest] {
  // 2
  return try await withThrowingTaskGroup(of: PhotoManifest.self) { group in
    let rovers = await fetchAllRovers()
    // 3
    try Task.checkCancellation()
    // 4
    for rover in rovers {
      group.addTask {
        return try await self.marsRoverAPI.photoManifest(rover: rover)
      }
    }
    // 5
    return try await group.reduce(into: []) { manifestArray, manifest in
      manifestArray.append(manifest)
    }
  }
}

Here’s a code breakdown:

  1. You do something a bit differently here to explore this API. fetchPhotoManifests() is a throwing async function.
  2. You use the withThrowingTaskGroup(of:returning:body:) function so the group can throw errors.
  3. Because downloading all the manifest data might take a while, check whether a parent task isn’t canceled before creating the child tasks. Task.checkCancellation() will throw a Task.CancellationError if there are any errors.
  4. If the parent task hasn’t been canceled, proceed to create child tasks to download the manifest for each rover.
  5. Using reduce(into:_:) is another way to loop through each task result and create an array to return.

In RoversListView.swift, add a property for MarsData and the manifests state:

let marsData = MarsData()
@State var manifests: [PhotoManifest] = []

Like latestPhotos, manifests stores the downloaded PhotoManifest when available.

Next, add a task to the NavigationView:

.task {
  manifests = []
  do {
    manifests = try await marsData.fetchPhotoManifests()
  } catch {
    log.error("Error fetching rover manifests: \(String(describing: error))")
  }
}

This code calls fetchPhotoManifests() and sets the view state with the result, catching any potential errors.

To display the manifests, replace MarsProgressView() inside the ZStack with:

List(manifests, id: \.name) { manifest in
  NavigationLink {
    Text("I'm sorry Dave, I'm afraid I can't do that")
  } label: {
    HStack {
      Text("\(manifest.name) (\(manifest.status))")
      Spacer()
      Text("\(manifest.totalPhotos) \(Image(systemName: "photo"))")
    }
  }
}
if manifests.isEmpty {
  MarsProgressView()
}

You create a List to contain all the navigation links to the rover manifests, using name, status and photo count. The text is only temporary, you’ll get to that soon.

Meanwhile, build and run and select the Rovers tab. Wow, those rovers have been busy!

iPhone simulator screen showing the rover list and total photos for each

OK, that’s a lot of photos! Too many photos to show in a single view.

Houston, we have a problem. But don’t fear because the Mars Rover Photos API has you covered. As you would expect, a NASA engineer designed the API to cover any eventuality.