This content originally appeared on DEV Community and was authored by ArshTechPro
What is a Task in Swift?
Think of a Task as a box that contains some work your app needs to do. In Swift’s async/await world, when you write async functions, you’re creating these boxes of work that can run at the same time.
func parentFunction() async {
// This is the main task (parent)
async let child1 = childFunction1() // Creates a child task
async let child2 = childFunction2() // Creates another child task
await [child1, child2] // Wait for both children to finish
}
The parent task creates child tasks, and they all form a family tree. The parent waits for its children to finish before it can finish.
What is @TaskLocal?
@TaskLocal
is like a magic backpack that gets passed down from parent tasks to their children automatically. You put something in the backpack once, and all the child tasks can see what’s inside without you having to manually hand it to each one.
Simple rules:
- Must be declared as
static
- Automatically shared with child tasks
- Only exists while the task is running
Example 1: Department Processing System
Let’s create a completely different scenario. Imagine you’re building a system where different departments process requests simultaneously:
enum Department {
@TaskLocal static var currentDepartment: String = "Unassigned"
}
func processRequests() async {
// Create tasks for different departments
let hrTask = Task {
await Department.$currentDepartment.withValue("HR") {
print(" \(Department.currentDepartment) department started processing")
try? await Task.sleep(nanoseconds: 500_000) // Simulate work
print(" \(Department.currentDepartment) department finished processing")
}
}
let itTask = Task {
await Department.$currentDepartment.withValue("IT") {
print(" \(Department.currentDepartment) department started processing")
try? await Task.sleep(nanoseconds: 700_000) // Simulate work
print(" \(Department.currentDepartment) department finished processing")
}
}
// Check what department we're in outside any task scope
print(" Main office department: \(Department.currentDepartment)")
// Wait for all departments to finish
await hrTask.value
await itTask.value
}
When you run this code, you might see output like:
IT department started processing
HR department started processing
Main office department: Unassigned
HR department finished processing
IT department finished processing
What’s happening here?
- Two departments (HR and IT) process requests concurrently
- Each task knows which department it belongs to
- The main office (outside any task) shows “Unassigned”
- Each department maintains its identity throughout the entire process
- The processing happens in parallel, so the order may vary
Example 2: Multiple Values with Nested Tasks
Now let’s see how one @TaskLocal
can store multiple values and work with child tasks:
struct AppContext {
let userID: String
let sessionToken: String
let requestID: String
}
enum AppSession {
@TaskLocal static var context: AppContext?
}
func handleAPIRequest() async {
let appContext = AppContext(
userID: "alice123",
sessionToken: "token_abc",
requestID: "req_789"
)
await AppSession.$context.withValue(appContext) {
await processRequest()
}
}
func processRequest() async {
logRequest()
// Create child tasks - they inherit the context automatically
async let dataFetch = fetchUserData()
async let validation = validateRequest()
await [dataFetch, validation]
logCompletion()
}
func logRequest() {
guard let ctx = AppSession.context else { return }
print("Starting request \(ctx.requestID) for user \(ctx.userID)")
}
func fetchUserData() async {
guard let ctx = AppSession.context else { return }
print("Fetching data for user: \(ctx.userID) with token: \(ctx.sessionToken)")
}
func validateRequest() async {
guard let ctx = AppSession.context else { return }
print("Validating request: \(ctx.requestID)")
}
func logCompletion() {
guard let ctx = AppSession.context else { return }
print("Completed request \(ctx.requestID) for user \(ctx.userID)")
}
Let’s run this:
await handleAPIRequest()
Output:
Starting request req_789 for user alice123
Fetching data for user: alice123 with token: token_abc
Validating request: req_789
Completed request req_789 for user alice123
What’s happening here?
- We set ONE context containing multiple values
- Both the main function AND child tasks (
async let
) can access all the values - No parameters needed – everything flows automatically through the task hierarchy!
Why is This Amazing?
Without @TaskLocal, you’d have to do this:
func processAPIRequest(user: UserContext) async {
await checkPermissions(user: user)
await fetchUserData(user: user)
await logActivity(user: user)
}
func checkPermissions(user: UserContext) async { ... }
func fetchUserData(user: UserContext) async { ... }
func logActivity(user: UserContext) async { ... }
You have to pass the user to every single function!
With @TaskLocal, you just set it once and forget about it:
await $currentUser.withValue(user) {
await processAPIRequest() // No parameters needed!
}
Key Benefits
- No more parameter passing – Set it once, use it everywhere
- Automatic inheritance – Child tasks automatically get the values
- Clean code – Functions don’t need extra parameters
- Safe – Values are only available in the right scope
Think of @TaskLocal
as a helpful assistant that automatically carries important information to everyone who needs it, so you don’t have to!
This content originally appeared on DEV Community and was authored by ArshTechPro