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 2 of 4 of this article. Click here to view the first page.

Creating a Task

You need to call latestPhotos(rover:) to start the process. In SwiftUI, a new view modifier runs an async task attached to a View.

In LatestView, add the following modifier to the HStack:

// 1
.task {
  // 2
  latestPhotos = []
  do {
    // 3
    let curiosityPhotos = try await latestPhotos(rover: "curiosity")
    let perseverancePhotos = try await latestPhotos(rover: "perseverance")
    let spiritPhotos = try await latestPhotos(rover: "spirit")
    let opportunityPhotos = try await latestPhotos(rover: "opportunity")
    // 4
    let result = [
      curiosityPhotos.first,
      perseverancePhotos.first,
      spiritPhotos.first,
      opportunityPhotos.first
    ]
    // 5
    latestPhotos = result.compactMap { $0 }
  } catch {
    // 6
    log.error("Error fetching latest photos: \(error.localizedDescription)")
  }
}

Here’s a step by step breakdown:

  1. task(priority:_:) takes a priority argument and a closure. The default priority is .userInitiated and that’s fine here. The closure is an asynchronous task to perform, in this case, fetch the photos from the API.
  2. You first set latestPhotos to an empty array so the progress view displays.
  3. Then you make four async API requests for rovers. Each call is marked await so the task waits until a request is complete before moving onto the next.
  4. Once all the requests finish, store the first photo from each in an array.
  5. You compact the resulting array to remove any nil values.
  6. Finally, you catch and log any errors.

Build and run your app. Latest Photos now shows a rotating Mars while the requests complete. Activity indicators appear for each MarsPhotoView while images download.

Your reward appears in the form of four new photos taken by the Mars rovers.

iPhone simulator screen showing a Mars image from Curiosity

The first test of your multistage entry sequence is a success! Excellent work, engineer.

At this point, spare a thought for Spirit and Opportunity, known as The Adventure Twins.

The NASA Mars rovers Spirit and Opportunity

Rest in peace, you brave little guys.

Multitasking

Your task(priority:_:) closure creates a single task that downloads the latest photos for each rover. One request processes at a time, waiting for each to complete before moving to the next.

Each time you call await, the task execution suspends while the request runs on another thread, which allows your app to run other concurrent code. Once the request is complete, your task resumes its execution until the next await. This is why you’ll often see await referred to as a suspension point.

In Swift 5.5, there are several ways you can structure code, so requests run concurrently. Instead of using the standard syntax let x = try await ... you’ll change your code to use the async let x = ... syntax instead.

In your .task modifier, replace the rover requests with this:

async let curiosityPhotos = latestPhotos(rover: "curiosity")
async let perseverancePhotos = latestPhotos(rover: "perseverance")
async let spiritPhotos = latestPhotos(rover: "spirit")
async let opportunityPhotos = latestPhotos(rover: "opportunity")

That might look a little odd. You declare that each let is async and you no longer need to use try or await.

All four tasks start immediately using that syntax, which isn’t surprising because you’re not using await. How they execute depends on available resources at the time, but it’s possible they could run simultaneously.

Notice Xcode is now complaining about the definition of result. Change this to:

let result = try await [
  curiosityPhotos.first,
  perseverancePhotos.first,
  spiritPhotos.first,
  opportunityPhotos.first
]

Now you use try await for the result array, just like a call to an async function.

async let creates a task for each value assignment. You then wait for the completion of all the tasks in the result array. Calling task(priority:_:) creates the parent task, and your four implicit tasks are created as child tasks of the parent.

Build and run. The app runs the same as before, but you might notice a speed increase this time.

iPhone simulator screen showing a nice Mars photo from Perseverance

Defining Structured Concurrency

The relationship between a child task and a parent task describes a hierarchy that’s the structure in structured concurrency.

The first version of your task closure created a single task with no parent, so it’s considered unstructured.

Unstructured concurrency diagram

The version that uses async let creates a task that becomes the parent of multiple child tasks. That’s structured concurrency.

Structured concurrency diagram

The task hierarchy can also be any number of layers deep because a child task can create its own children, too. The distinction between structured and unstructured concurrency might seem academic, but keep in mind some crucial differences:

  • All child tasks and descendants automatically inherit the parent’s priority.
  • All child tasks and descendants automatically cancel when canceling the parent.
  • A parent task waits until all child tasks and descendants complete, throw an error or cancel.
  • A throwing parent task can throw if any descendant task throws an error.

In comparison, an unstructured task inherits priority from the calling code, and you need to handle all of its behaviors manually. If you have several async tasks to manage simultaneously, it’s far more convenient to use structured concurrency because you only need await the parent task or cancel the parent task. Swift handles everything else!

Canceling Tasks

You used task(priority:_:) in LatestView.swift to run an asynchronous task. This is the equivalent of creating a Task directly using init(priority:operation:). In this case, the task is tied to the view’s lifecycle.

If the view is updated while the task is running, it’ll be canceled and recreated when the view is redisplayed. If any task is canceled, it’s marked as canceled but otherwise continues to run. It’s your responsibility to handle this matter by cleaning up any resources that might still be in use and exit early.

In any async task code, you can check the current task cancellation status by either observing whether Task.isCancelled is true or by calling Task.checkCancellation(), which will throw CancellationError.

Fortunately for you, engineers from Apple ensured URLSession tasks will throw a canceled error if the task is canceled. That means your code will avoid making unnecessary network requests.

However, if you have some long-running or resource-intensive code, be aware it’ll continue to run until finished even though the task is canceled. So it’s good to check the cancellation status at appropriate points during your task’s execution.