SwiftUI and Structured Concurrency
Learn how to manage concurrency into your SwiftUI iOS app. By Andrew Tetlaw.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SwiftUI and Structured Concurrency
30 mins
- Getting Started
- Downloading a Photo
- Responding to Download Errors
- Using the Mars Rover API
- Fetching Rover Images
- Creating a Task
- Multitasking
- Defining Structured Concurrency
- Canceling Tasks
- Fetching All Rovers
- Creating a Task Group
- Exploring All Photos
- Displaying the Rover Manifests
- Presenting the Photos
- Browse by Earth Date
- Where to Go From Here?
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:
- Your new async function will return an array of
Photo
. - You use
withTaskGroup(of:returning:body:)
to create the task group and specify that each task in the group will return an optionalPhoto
.
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:
- Loop through each rover.
- Call
addTask(priority:operation:)
on thegroup
and pass a closure representing the work the task needs to perform. - In the task, you call
latestPhotos(rover:)
and then return the first photo if any were retrieved, ornil
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:
- First you set up an empty array to hold the returned photos.
- Then you create a
for
loop that loops through the asynchronous sequence provided by the group, waiting for each value from the child task. - If the photo isn’t
nil
, you append it to thelatestPhotos
array. - 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.
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.
async {
// ...
}
or adding a group task with:
group.async {
//...
}
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.
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:
- You do something a bit differently here to explore this API.
fetchPhotoManifests()
is a throwing async function. - You use the
withThrowingTaskGroup(of:returning:body:)
function so the group can throw errors. - 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 aTask.CancellationError
if there are any errors. - If the parent task hasn’t been canceled, proceed to create child tasks to download the manifest for each rover.
- 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!
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.