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?
Swift 5.5 delivered an exciting frontier to explore: Swift Concurrency. If you’ve had any experience writing asynchronous code or using asynchronous APIs, you’re familiar with how complicated it can be. Debugging asynchronous code can make you feel like you’re on another world, perhaps even Mars!
The new Swift Concurrency API promises a simpler, more readable way to write asynchronous and parallel code. The more you explore the landscape of Swift Concurrency, the more you’ll discover the sophistication provided by a simple API.
In this tutorial, you’ll build Roving Mars, an app that lets you follow the Mars rovers and see the photos they take daily during their missions.
Along the way, you’ll learn:
- How to use
AsyncImage
in SwiftUI to manage the presentation of remote images. - The difference between structured and unstructured concurrency in Swift.
- How to use a
TaskGroup
to handle concurrent asynchronous tasks.
Getting Started
Use Download Materials at the top or bottom of this tutorial to download the starter project. Open it in Xcode and run it to see what you have to work with.
The first tab shows the latest photos from the Mars rovers. The second tab lets users explore all the available photos.
Your app is pretty empty at the moment. Hurry, space awaits!
Downloading a Photo
AsyncImage
is one of SwiftUI’s newest features. Every mobile app developer has dealt with downloading an image asynchronously, showing a placeholder while it downloads and then showing the downloaded image when it’s available. AsyncImage
wraps this whole process in a simple wrapper.
Time to don your space suit and start exploring.
Open LatestView.swift from the starter project. Replace MarsProgressView()
with:
AsyncImage(
//1
url: URL(string: "https://mars.nasa.gov/msl-raw-images/proj/msl/redops/ods/surface/sol/03373/opgs/edr/ncam/NRB_696919762EDR_S0930000NCAM00594M_.JPG")
//2
) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
//3
} placeholder: {
MarsProgressView()
}
Here’s a step by step breakdown:
- Here’s the image’s URL. It’s straight from the NASA collection.
- This closure is designed to output the view you’ll display when the image downloads. The closure argument is an
Image
, which you can return directly or you can add your own views or modifiers. In this code you’re making the image resizable and setting the aspect ratio to fit the available space. - This closure outputs the placeholder displayed while the image downloads. You use the
MarsProgressView
that’s already prepared for you, but a standardProgressView
also works.
Build and run the app. First, you’ll see the placeholder. Then the Image
will appear once the download is complete.
That worked well! But what if you encounter an error while downloading? You’ll address that issue next.
Responding to Download Errors
AsyncImage
has an alternative that allows you to respond to a download error. Replace your first AsyncImage
with:
AsyncImage(
url: URL(string: "https://mars.nasa.gov/msl-raw-images/proj/msl/redops/ods/surface/sol/03373/opgs/edr/ncam/NRB_696919762EDR_S0930000NCAM00594M_.JPG")
) { phase in
switch phase {
//1
case .empty:
MarsProgressView()
//2
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
//3
case .failure(let error):
VStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text(error.localizedDescription)
.font(.caption)
.multilineTextAlignment(.center)
}
@unknown default:
EmptyView()
}
}
The closure is passed an AsyncImagePhase
enum value, of which there are three cases:
-
.empty
: An image isn’t available yet, so this is the perfect spot for the placeholder view. -
.success(let image)
: An image was successfully downloaded. This value contains an image you can output as you like. -
.failure(let error):
: If an error occurred while downloading the image, you use this case to output an alternative error view. In your app, you show a warning symbol from SF Symbols and thelocalizedDescription
of the error.
Build and run your app, and you’ll see it works the same as before.
To test the placeholder, change the URL
argument to nil
. Or, try testing the error by changing the domain in the URL
string to a nonexistent domain such as "martianroversworkersunion.com"
.
So far, so good. But how do you display the latest photos from all the rovers?
Using the Mars Rover API
That image URL is from the NASA archive, which also has many public APIs available. For this app, your data source is the Mars Rover Photos API.
First, you need to get an API key. Visit NASA APIs and fill in the Generate API Key form. You’ll need to append the API key to all the API requests your app makes.
Once you’ve obtained your key, and to test that it works, enter the following URL into a browser address field and replace DEMO_KEY
with your API key.
https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/latest_photos?api_key=DEMO_KEY
You’ll see a large JSON payload returned.
All Mars Rover Photos API requests return a JSON response. In MarsModels.swift you’ll find all the Codable
structs to match each type of response.
Now open MarsRoverAPI.swift and find the line at the top of the file that reads:
let apiKey = "YOUR KEY HERE"
Replace YOUR KEY HERE
with the API key you obtained from the NASA site.
It’s now time to align your dish and make contact!
Fetching Rover Images
You first need to request the latest photos. Open LatestView.swift and add this to LatestView
:
// 1
func latestPhotos(rover: String) async throws -> [Photo] {
// 2
let apiRequest = APIRequest<LatestPhotos>(
urlString: "https://api.nasa.gov/mars-photos/api/v1/rovers/\(rover)/latest_photos"
)
let source = MarsRoverAPI()
// 3
let container = try await source.request(apiRequest, apiKey: source.apiKey)
// 4
return container.photos
}
Here’s how this works:
- You make
latestPhotos(rover:)
a throwing async function because theMarsRoverAPI
function that it calls is also throwing and async. It returns an array ofPhoto
. - Use
APIRequest
to specify the URL to call and how to decode the JSON response. - You call the
apiRequest
endpoint and decode the JSON response. - Return a
Photo
array.
Now in LatestView
, add a state property to hold the photos:
@State var latestPhotos: [Photo] = []
In MarsImageView.swift you’ll also find a view that incorporates the AsyncImage
you already built. It takes a Photo
and displays the image along with some interesting information.
Since you’ll display several photos, you need to present them using ForEach
.
Back in LatestView.swift, replace your previous test AsyncImage
with:
// 1
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(latestPhotos) { photo in
MarsImageView(photo: photo)
.padding(.horizontal, 20)
.padding(.vertical, 8)
}
}
}
// 2
if latestPhotos.isEmpty {
MarsProgressView()
}
Here’s a code breakdown:
- You make a
ScrollView
containing aHStack
and loop through thelatestPhotos
, creating aMarsImageView
for eachPhoto
. - While
latestPhotos
is empty, you display theMarsProgressView
.
Build and run the app to see the latest Mars photos.
Wait … nothing is happening. The app only shows the loading animation.
You have a stage missing.