Structured and Unstructured Tasks in Swift
What Is a Task?
A Task in Swift is a container for code that runs independently without freezing the app. While one task downloads data from the internet, another can update the screen simultaneously – keeping the app responsive. Tasks enable multiple operations to happen at the same time rather than waiting for each one to finish sequentially.
What Are Structured Tasks?
Structured tasks work like a family tree – parent tasks automatically manage their child tasks. Created using async let
or TaskGroup
, these tasks inherit settings from their parent and must finish before the parent can complete. When a parent task ends, all its children end too. This automatic management ensures nothing gets left running accidentally.
What Are Unstructured Tasks?
Unstructured tasks run independently, like separate apps on a phone. Created with Task { }
or Task.detached { }
, these tasks manage themselves and keep running even after the code that started them finishes. This independence makes them perfect for operations that need to continue in the background, like saving user data after a button tap.
Key Differences
Lifecycle Management
Structured tasks automatically terminate when their parent scope ends, ensuring no orphaned tasks exist. Unstructured tasks continue running independently and require explicit management for cancellation or completion tracking.
Context Inheritance
Structured tasks inherit task-local values, priority, and cancellation from their parent. Unstructured tasks created with Task { }
inherit some context like actor isolation, while Task.detached { }
starts completely fresh without any inherited context.
Cancellation Propagation
When a parent structured task is cancelled, all child tasks are automatically cancelled. Unstructured tasks must check for cancellation explicitly and handle it appropriately.
Scope Boundaries
Structured tasks cannot escape their defining scope – the parent waits for all children to complete. Unstructured tasks can outlive the scope that created them, providing flexibility for long-running operations.
Example 1: Structured Task Using TaskGroup
func fetchMultipleUserProfiles() async throws -> [UserProfile] {
return try await withThrowingTaskGroup(of: UserProfile.self) { group in
let userIDs = [1, 2, 3, 4, 5]
// Create child tasks within the group
for id in userIDs {
group.addTask {
// This task inherits cancellation from parent
// and cannot outlive the TaskGroup
return try await fetchUserProfile(id: id)
}
}
// Parent automatically waits for all children
var profiles: [UserProfile] = []
for try await profile in group {
profiles.append(profile)
}
return profiles
}
// All child tasks are guaranteed to be complete here
}
In this example, the TaskGroup
creates a structured environment where all child tasks are automatically managed. If the parent function is cancelled, all profile fetching operations are cancelled. The function cannot return until all child tasks complete or throw an error.
Example 2: Unstructured Task for Independent Operation
class ViewController: UIViewController {
var analyticsTask: Task<Void, Never>?
@IBAction func buttonTapped(_ sender: UIButton) {
// Create an unstructured task that runs independently
analyticsTask = Task {
// This task continues even if the button handler returns
await logUserInteraction(action: "button_tap")
// Check cancellation manually if needed
if Task.isCancelled { return }
await uploadAnalytics()
}
// The button handler returns immediately
// The analytics task continues running independently
updateUI()
}
deinit {
// Manual cleanup required for unstructured tasks
analyticsTask?.cancel()
}
}
This unstructured task handles analytics independently of the button tap handler’s scope. The task continues running after the action method returns, demonstrating how unstructured tasks provide flexibility for operations that need to outlive their creation context. Manual cancellation in deinit
ensures proper cleanup.
Conclusion
The choice between structured and unstructured tasks depends on the specific requirements of the operation. Structured tasks excel when work has clear boundaries and parent-child relationships, providing automatic resource management and cancellation propagation. Unstructured tasks offer flexibility for independent operations that need to transcend scope boundaries, though they require more careful manual management. Understanding these differences enables developers to choose the appropriate concurrency pattern for each situation in their Swift applications.