Hey Swifters 👋 I this guide we will explore @preconcurrency in Swift.
You have probably been loving the new Swift Concurrency features async/await
, Task
, actor
, and the rest of the crew. But sometimes you have to interact with older code, the kind that wasn’t written with concurrency in mind. And that’s when you might stumble upon this mysterious macro @preconcurrency
.
Let’s break it down. What is it? Why is it there? And when should you use it?
So. What is @preconcurrency
?
@preconcurrency
is Swift’s way of saying
“Hey compiler, I know this type or method wasn’t built with Swift Concurrency in mind, but I promise I’ll use it safely, treat it like it’s Sendable or safe to access across concurrency boundaries even if you can’t be sure.”
In other words, it’s a compatibility hack, a safe bridge between old synchronous code and the new concurrent world.
Now, Why Do We Need It?
Swift Concurrency introduced strict rules around what can safely be used from concurrent code.
- Types used across threads should conform to
Sendable
. - You should avoid accessing shared mutable state from multiple tasks.
- And the compiler checks all of this for you.
But older libraries, system frameworks, or legacy types don’t always have the right annotations. Without @preconcurrency
, you will start seeing warnings or errors like:
“Type ‘XYZ’ does not conform to ‘Sendable’”
“Call to main actor-isolated instance method in a synchronous context”
That’s where @preconcurrency
comes in handy.
A Real-World Example
Let’s say you are using an old DataFetcher
class from a library written in Swift 5.4 (before concurrency came along).
class DataFetcher {
func fetchData(completion: @escaping (String) -> Void) {
// some legacy networking logic
}
}
Now, in Swift 5.7+, if you try to use this from a Task, the compiler might complain:
let fetcher = DataFetcher()
Task {
fetcher.fetchData { result in
print(result)
}
}
Even though you’re using it in an async context, Swift doesn’t know if DataFetcher
is safe to use concurrently and it has no @Sendable
or actor isolation. Here is how you can fix it using @preconcurrency
.
@preconcurrency class DataFetcher {
func fetchData(completion: @escaping (String) -> Void) {
// legacy code
}
}
Now Swift says:
“Okay, I’ll assume this class was safe before concurrency was introduced. You’re responsible for using it correctly.”
Where Can You Use @preconcurrency
?
You can put it on:
class
protocol
extension
function
typealias
import
statements
But Is It Safe?
That’s the catch. @preconcurrency
is you telling the compiler: “Trust me.”
So it’s only safe if:
- The type won’t be mutated from multiple tasks at once
- The method doesn’t access shared mutable state
- You’re not introducing race conditions
Otherwise, it’s just a shiny band-aid over a potential bug.
So use it with care. It doesn’t make old code magically thread-safe. It just silences Swift’s new warnings so you can keep moving forward.
See you in the next one 😉
More on Swift Concurrency: