Skip to main content

Swift Task API

Introduction

Swift's Task API is a fundamental part of the Swift Concurrency model that allows you to create, manage, and interact with asynchronous operations. Tasks represent units of work that can run concurrently with other code, making it easier to write non-blocking, efficient applications. Whether you're fetching data from a remote server, processing images, or performing any operation that shouldn't block your main thread, the Task API provides a structured way to handle these operations.

In this guide, we'll explore the Swift Task API in detail, from basic concepts to practical applications, with plenty of examples to help you understand how tasks work in Swift.

What is a Task?

A task in Swift represents an asynchronous operation that can be started, monitored, canceled, and awaited upon. Tasks are the building blocks of concurrent code in Swift, allowing you to perform work in the background while keeping your application responsive.

Key Characteristics of Tasks

  • Independent execution: Tasks run independently of the code that creates them
  • Structured concurrency: Tasks form a hierarchy, with parent tasks and child tasks
  • Cancelable: Tasks can be canceled, which propagates to their child tasks
  • Priority-aware: Tasks run with specific priorities that influence scheduling

Creating Tasks

Swift provides several ways to create tasks depending on your needs:

Basic Task Creation

The simplest way to create a task is using the Task initializer:

swift
let task = Task {
// Perform some asynchronous work
try await fetchData()
}

// Later, you can await the result
let result = try await task.value

Task with Priority

You can specify a priority when creating a task:

swift
let highPriorityTask = Task(priority: .high) {
try await performImportantWork()
}

let backgroundTask = Task(priority: .background) {
try await performNonUrgentWork()
}

Available priorities include .high, .medium, .low, .background, and .userInitiated.

Detached Tasks

Regular tasks are linked to the context that created them. If you need a task that exists independently of its creator's context, use a detached task:

swift
let detachedTask = Task.detached {
try await performBackgroundWork()
}

Note: Use detached tasks sparingly, as they don't inherit cancellation or priority from their parent context.

Working with Task Results

Awaiting a Task's Result

To get the result from a task, use await with the task's value property:

swift
func loadUserData() async throws {
let userTask = Task {
try await fetchUserFromNetwork(id: "user123")
}

// Do other work while the task is running...

// When you need the result:
let user = try await userTask.value
print("Loaded user: \(user.name)")
}

Handling Task Cancellation

Tasks can be canceled, and well-written async code should check for cancellation:

swift
let downloadTask = Task {
var downloadedBytes = 0

while downloadedBytes < totalBytes {
// Check if task was canceled
try Task.checkCancellation()

// Download next chunk
let chunk = try await downloadNextChunk()
downloadedBytes += chunk.count
}

return downloadedBytes
}

// To cancel the task:
downloadTask.cancel()

You can check for cancellation with:

swift
if Task.isCancelled {
// Clean up and exit
return
}

Child Tasks

Tasks can create child tasks, which inherit properties from their parent:

swift
func processImages(urls: [URL]) async throws -> [ProcessedImage] {
var results: [ProcessedImage] = []

// Parent task
for url in urls {
// Child task
let processedImage = try await Task {
// If parent is canceled, this child is automatically canceled
let imageData = try await downloadImage(from: url)
return try await processImage(imageData)
}.value

results.append(processedImage)
}

return results
}

Task Groups

When you need to run multiple similar tasks concurrently and collect their results, use task groups:

swift
func downloadAndProcessImages(urls: [URL]) async throws -> [ProcessedImage] {
try await withThrowingTaskGroup(of: ProcessedImage.self) { group in
for url in urls {
group.addTask {
let imageData = try await downloadImage(from: url)
return try await processImage(imageData)
}
}

var processedImages: [ProcessedImage] = []
for try await result in group {
processedImages.append(result)
}
return processedImages
}
}

Task Group Features

  • Automatically cancels all child tasks if one fails
  • Provides a way to iterate through results as they become available
  • Ensures all tasks complete (or are canceled) before the function returns

Task.sleep for Testing and Delays

Task.sleep is useful for testing async code or implementing delays:

swift
func simulateNetworkRequest() async throws -> String {
// Simulate network delay
try await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
return "Response data"
}

Note: Task.sleep is not the same as thread sleep - it suspends only the current task, not the entire thread.

Practical Example: Image Loading App

Let's put everything together in a practical example for an image loading app:

swift
class ImageLoader {
private var currentTask: Task<UIImage, Error>?

func loadImage(from url: URL) async throws -> UIImage {
// Cancel any existing task
currentTask?.cancel()

// Create new task with medium priority
currentTask = Task(priority: .medium) {
// Check for cancellation before network call
try Task.checkCancellation()

let (data, _) = try await URLSession.shared.data(from: url)

// Check for cancellation before CPU-intensive work
try Task.checkCancellation()

guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}

return image
}

// Return the task's result
return try await currentTask!.value
}

func loadMultipleImages(urls: [URL]) async throws -> [UIImage] {
try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in
// Add a task for each URL with its index
for (index, url) in urls.enumerated() {
group.addTask {
let image = try await self.loadImage(from: url)
return (index, image)
}
}

// Create a result array of the correct size
var images = [UIImage?](repeating: nil, count: urls.count)

// Process results as they arrive
for try await (index, image) in group {
images[index] = image
}

// Remove any nil values in case of failures
return images.compactMap { $0 }
}
}

enum ImageError: Error {
case invalidData
}
}

Usage example:

swift
func updateUI() async {
do {
let imageLoader = ImageLoader()
let profileImage = try await imageLoader.loadImage(from: profileImageURL)

// Update UI on the main thread
await MainActor.run {
profileImageView.image = profileImage
loadingSpinner.stopAnimating()
}

// Load gallery images in parallel
let galleryImages = try await imageLoader.loadMultipleImages(urls: galleryImageURLs)

await MainActor.run {
updateGallery(with: galleryImages)
}
} catch {
await MainActor.run {
showError(error.localizedDescription)
}
}
}

Task Local Values

Task local values provide a way to set values within the context of a task that can be accessed by that task and any child tasks:

swift
@TaskLocal
static var currentRequestID: String?

func processRequest() async throws {
try await $currentRequestID.withValue("request-123") {
// Value is available within this scope and child tasks
print("Processing \(Self.$currentRequestID ?? "unknown")")

// Child tasks inherit the value
try await performSubtask()
}
}

func performSubtask() async throws {
// Can access the value set by the parent
print("Subtask for \(Self.$currentRequestID ?? "unknown")")
}

This is particularly useful for propagating contextual information like request IDs or user sessions through an async call hierarchy.

Summary

Swift's Task API provides a powerful framework for handling asynchronous operations in a structured and efficient way:

  • Tasks represent individual units of asynchronous work
  • Task creation can be customized with priorities and detachment options
  • Tasks can be awaited, canceled, and checked for completion
  • Task groups allow easy management of multiple concurrent operations
  • Task local values help propagate contextual information

Understanding the Task API is essential for writing modern Swift applications that remain responsive while handling complex asynchronous operations.

Further Resources

Exercises

  1. Create a task that downloads data from three different URLs concurrently and combines the results.
  2. Implement a task that performs a lengthy operation but checks for cancellation periodically.
  3. Write a function using task groups that downloads images from an array of URLs but limits concurrency to a maximum of 4 simultaneous downloads.
  4. Experiment with task priorities by creating high and low priority tasks and observing their execution order.
  5. Implement a task local value to track and log the execution time of tasks in a hierarchy.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)