Let me start by saying: you probably don’t need this feature in general Swift projects. Yet, I did want to highlight this feature in my course for those more advanced cases.
A task-local value is a value that can be bound to the context of a Task. This might ring a bell—yes, task local storage will be passed through to any child tasks.
What is a @TaskLocal value?
A common example of a task local value is the cancellation state of a task. As we learned, we can read such value as follows:
/// Check for cancellation before the network request starts.
guard !Task.isCancelled else {
return fallbackImage
}
Task local values must be declared as static properties, global properties, or by using the @TaskLocal attribute in Swift 6 and above. You could say the isCancelled property is defined inside the standard library as follows:
extension Task {
@TaskLocal
static var isCancelled: Bool = false
}
It’s a static property, attributed with @TaskLocal.
Defining a custom Task Local value
Task local values are easier to understand with an actual example.
For example, let’s say we want to define a user ID as a local value. Obviously, you could normally pass this value around directly as it’s Sendable, but for the sake of this example, we’re using a local value:
enum UserContext {
@TaskLocal
static var userId: Int?
}
The @TaskLocal macro allows us to use the userId property as follows:
UserContext.$userId.withValue(123) {
print("Inside Task - User ID: \(UserContext.userId ?? -1)")
// Prints: 123
Task {
print("Nested Task - User ID: \(UserContext.userId ?? -1)")
// Prints: 123
}
Task.detached {
print("Detached Task - User ID: \(UserContext.userId ?? -1)")
// Prints: -1 (local value not inherited)
}
}
Task local values cannot be set directly and you must use the withValue() { ... } method accordingly. The value is only bound for the duration of that closure scope, and is available to any child tasks which are created within that scope.
We can confirm this by looking at the print statements in the above example. It’s also clear that detached tasks, which are unstructured and don’t inherit context, can’t access the local value. This is similar to reading the cancellation state as we’ve learned before.
Should I ever use task local values?
In my opinion, you should not. They can complicate things and you risk scenarios where you expect a value to exist as a requirement while it’s actually not set. Far better would be to simply pass around the value you need directly within the method’s properties.
Summary
Task local values allow you to bind a value to the scope of a structured task. We’ve learned that it’s explaining how a task’s cancellation state becomes accessible. While task local values are an interesting feature, it’s often far better to rely on predictable property injection rather than risking the existance of a local value inside the current context.
In the next lesson, we’re going to look into using tasks within SwiftUI.