engomarelsayed/swift-concurrent-macro
A Swift macro that keeps your heavy work off the main actor — automatically.
The Problem
Since Swift 6.2 (SE-0461, enabled via the NonisolatedNonsendingByDefault upcoming feature — part of Xcode's "Approachable Concurrency" setting), nonisolated async functions run on the caller's actor by default.
This is a great default for most code. But it has a sneaky side effect: if you call this from the main actor, all of its synchronous work now happens on the main actor.
struct ImageCompressor {
func compress(_ image: Data) async -> Data {
// Called from a SwiftUI view? Congratulations,
// you're now compressing images on the main thread. 😅
}
}The fix is the @concurrent attribute, which guarantees the function runs on the global concurrent executor:
struct ImageCompressor {
@concurrent func compress(_ image: Data) async -> Data { ... }
}That works — until you have a type whose entire purpose is heavy CPU-bound work. Image compression, decoding large JSON payloads, hashing files, data processing in use cases. Now every single async method needs the annotation:
struct ImageCompressor {
@concurrent func compress(_ image: Data) async -> Data { ... }
@concurrent func resize(_ image: Data, to size: CGSize) async -> Data { ... }
@concurrent private func encode(_ image: CGImage) async -> Data { ... }
}Forget one, and there's no compiler error. No warning. Just heavy work quietly running on the main actor — exactly the performance bug the attribute exists to prevent.
The Solution
One annotation on the type. Done.
@Concurrent
struct ImageCompressor {
func compress(_ image: Data) async -> Data { ... }
func resize(_ image: Data, to size: CGSize) async -> Data { ... }
private func encode(_ image: CGImage) async -> Data { ... }
}It works on struct, class, enum — and extensions, so you can scope it to just the part of a type that does the heavy lifting:
struct FetchDataUseCase { ... }
@Concurrent
extension FetchDataUseCase {
// @concurrent is added only to async methods in this extension
func process(_ response: Response) async -> Model { ... }
}Installation
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/EngOmarElsayed/swift-concurrent-macro", from: "1.0.0")
]Then add it to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "ConcurrentMacro", package: "swift-concurrent-macro")
]
)Or in Xcode: File → Add Package Dependencies… and paste the repo URL.
import ConcurrentMacroRequirements
- Swift 6.2+
- The
NonisolatedNonsendingByDefaultupcoming feature enabled (or Xcode's "Approachable Concurrency" setting, which includes it)
Note: Without SE-0461's new behavior enabled, nonisolated
asyncfunctions already run on the global concurrent executor, so this macro isn't doing anything you need. It exists specifically for codebases adopting Swift 6.2's new defaults.
How It Works
The macro follows a few simple rules — no surprises:
| Rule | Behavior | |---|---| | Only async methods | Synchronous methods are untouched. @concurrent only changes where nonisolated async functions execute, so the macro skips sync methods entirely. | | Implies nonisolated | @concurrent already implies nonisolated. Under defaultIsolation = MainActor (SE-0466), the annotated methods run on the global concurrent executor instead of the main actor. | | Explicit isolation wins | Methods explicitly marked @MainActor, nonisolated(nonsending), or with any other explicit isolation are skipped. Your per-method decisions always take priority. | | No actors | Applying @Concurrent to an actor contradicts the whole point of actor isolation, so the macro emits a compile-time error. |
When Should You Use This?
Be honest with yourself here: most async code doesn't need @concurrent at all. Functions that suspend immediately and wait (like network calls) don't block the caller's actor — the suspension already frees it.
Reach for @Concurrent when a type does CPU-bound work between suspension points:
- Image compression and processing pipelines
- Decoding large JSON payloads
- Hashing or encrypting files
- Data transformation in use cases after fetching from a backend
If that describes the whole type, annotate the type. If it describes one method, just use @concurrent directly — this package isn't trying to replace it.
Background
This started as a Swift Evolution pitch. The feedback nudged it toward the package-first route — prove it in the ecosystem, evolve it freely, and maybe revisit stdlib inclusion later. So here we are.
Want to go deeper into Swift concurrency? I write full deep-dives on exactly these topics at swiftdifferently.com.
Contributing
Found a bug? Have an idea? Issues and PRs are very welcome — that's the beauty of this being a package instead of the stdlib: we can actually fix things today. 😂
License
swift-concurrent-macro is available under the MIT license. See LICENSE for details.
Made with ❤️ by Omar Elsayed.
Package Metadata
Repository: engomarelsayed/swift-concurrent-macro
Default branch: main
README: README.md