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

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.
Note: This intermediate-level tutorial assumes you’re comfortable building an iOS app using Xcode and Swift. You should have used SwiftUI and have at least a beginner understanding of Swift async and await.

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:

  1. Here’s the image’s URL. It’s straight from the NASA collection.
  2. 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.
  3. This closure outputs the placeholder displayed while the image downloads. You use the MarsProgressView that’s already prepared for you, but a standard ProgressView also works.

Build and run the app. First, you’ll see the placeholder. Then the Image will appear once the download is complete.

iPhone simulator screen showing a martian landscape photo

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:

  1. .empty: An image isn’t available yet, so this is the perfect spot for the placeholder view.
  2. .success(let image): An image was successfully downloaded. This value contains an image you can output as you like.
  3. .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 the localizedDescription 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".

3 iPhone simulator screens showing the placeholder, photo and error message

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.

browser window showing JSON output

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.

Note: The API documentation is available on NASA APIs, and also on the maintainer’s GitHub repository.

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:

  1. You make latestPhotos(rover:) a throwing async function because the MarsRoverAPI function that it calls is also throwing and async. It returns an array of Photo.
  2. Use APIRequest to specify the URL to call and how to decode the JSON response.
  3. You call the apiRequest endpoint and decode the JSON response.
  4. 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:

  1. You make a ScrollView containing a HStack and loop through the latestPhotos, creating a MarsImageView for each Photo.
  2. While latestPhotos is empty, you display the MarsProgressView.

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.