Bridge modern Swift concurrency (async/await) to completion‑handler based APIs using a tiny set of Task convenience initializers. Keep your existing synchronous‑style public interface while migrating internals to async/await.
It isn’t common to wrap an async function with a completion handler — usually, we do the reverse. But this becomes useful when some public API can’t change yet while your implementation is moving to async/await. This package provides small, focused helpers to:
- Run an async operation in a
Task. - Deliver results to a completion handler.
- Choose where the completion runs (MainActor or a specific DispatchQueue).
- Avoid boilerplate and reduce the risk of incorrect callback queues.
- Unlabeled
operationclosure for ergonomic, trailing closure syntax (just like Swift’s nativeTaskinitializers) - MainActor delivery for UI‑safe callbacks using
await MainActor.run { ... } - DispatchQueue delivery when you need a specific GCD queue
- Overloads for:
- async throwing returning a value →
Result<T, Error>completion - async throwing returning
Void→Error?completion - async returning a value →
(T) -> Voidcompletion - async returning
Void→() -> Voidcompletion
- async throwing returning a value →
@Sendableclosures andT: Sendableconstraints to help prevent data races across concurrency boundaries- Public APIs are marked
@inlinablefor maximal inlining and performance
The library can be helpful when:
- Apple frameworks mandate completion handlers (WidgetKit, some UIKit patterns) - example
- Public API compatibility is non-negotiable (SDKs, frameworks)
- Gradual migration from legacy codebases with mixed paradigms
- Objective-C interoperability requirements (completion handlers bridge better than async/await)
- Testing infrastructure hasn’t fully adopted Swift Concurrency yet
The library eliminates boilerplate, ensures thread safety with
Sendableconstraints, and defaults toMainActordelivery for UI safety—making it significantly better than ad-hocTaskwrapping.
Add package dependency
import AsyncToSyncBridge
// Suppose you have "Modern Concurrency" API
func didReceiveRemoteNotification(userInfo:[AnyHashable: Any]) async -> UIBackgroundFetchResult {
// ...
return .newData
}
// But you need to use it inside a synchronous completion-based API, i.e. `UIApplicationDelegate`
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// With trailing closure, this looks like native Task initializers:
Task {
await didReceiveRemoteNotification(userInfo:userInfo)
} completion: { value in
completionHandler(value)
}
}
And other overloads for other types of completions are available:
```swift
// Async throwing returning a value → Result<T, Error> on MainActor
Task {
try await doWorkReturningValue()
} completion: { (result: Result<MyType, Error>) in
// runs on MainActor
}
// Async throwing returning Void → Error? on MainActor
Task {
try await doWorkThrowingVoid()
} completion: { (error: Error?) in
// runs on MainActor; error is nil on success
}
// Async returning a value → (T) -> Void on MainActor
Task {
await doWorkReturningValue()
} completion: { value in
// runs on MainActor when finished
}
// Async returning Void → () -> Void on MainActor
Task {
await doWorkVoid()
} completion: {
// runs on MainActor when finished
}If you must call your completion handler on a specific DispatchQueue (for example, a background queue, or for legacy code):
- Public API is marked @inlinable for performance and cross-module optimization.
- Trailing closure syntax makes adoption seamless and familiar for Swift developers.
- The difference between MainActor and .main queue is clearly documented and enforced at the API level.
Task(queue: .main) {
try await doWorkReturningValue()
} completion: { (result: Result<MyType, Error>) in
// runs on DispatchQueue.main (GCD), NOT MainActor
}- DispatchQueue.main.async is not the same as await MainActor.run {}.
- Use the MainActor overloads for UI updates and actor isolation.
- Use the DispatchQueue variants for legacy queue requirements.