bonkey/AwaitlessKit
Simplifies the migration to async/await. It likely performs better than your ad hoc hacks.
Table of Contents <!-- omit in toc -->
Quick Start
Add the @Awaitless macro to your async functions to automatically generate synchronous wrappers:
import AwaitlessKit
class DataService {
@Awaitless
func fetchUser(id: String) async throws -> User {
// Your async implementation
}
// Generates: @available(*, noasync) func fetchUser(id: String) throws -> User
}
// Use both versions during migration
let service = DataService()
let user1 = try await service.fetchUser(id: "123") // Async version
let user2 = try service.fetchUser(id: "456") // Generated sync versionSee more examples or documentation for more sophisticated cases.
Why AwaitlessKit?
The Problem: Swift's async/await adoption is an "all-or-nothing" proposition. You can't easily call async functions from sync contexts, making incremental migration painful.
The Solution: AwaitlessKit automatically generates synchronous counterparts for your async functions, allowing you to:
- ✅ Migrate to
async/awaitincrementally - ✅ Maintain backward compatibility during transitions
- ✅ Avoid rewriting entire call chains at once
- ✅ Keep existing APIs stable while modernizing internals
⚠️ Important: This library bypasses Swift's concurrency safety mechanisms. It is a migration tool, not a permanent solution.
Installation
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/bonkey/AwaitlessKit.git", from: "9.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["AwaitlessKit"]
)
]Swift 6.0+ compiler required (available in Xcode 16 and above).
Core Features
AwaitlessKit provides bidirectional conversion between async/await and legacy callback-based APIs:
@Awaitless* Family - async → sync conversion
Generates synchronous wrappers for async functions with built-in deprecation controls.
@Awaitless- Generates synchronous throwing functions that can be called directly from non-async contexts@AwaitlessPublisher- Generates CombineAnyPublisherwrappers for reactive programming patterns@AwaitlessCompletion- Generates completion-handler based functions usingResultcallbacks
@Awaitable* Family - sync → async conversion
Generates async/await wrappers for legacy callback-based functions, enabling migration to modern Swift concurrency.
@AwaitablePublisher- Converts CombineAnyPublisherfunctions to async/await using.async()@AwaitableCompletion- Converts completion-handler functions to async/await usingwithCheckedThrowingContinuation@Awaitable- Protocol macro that generates async versions of both Publisher and completion-handler methods
PromiseKit Integration
Bidirectional PromiseKit integration (separate AwaitlessKit-PromiseKit product):
@AwaitlessPromise- async → Promise conversion@AwaitablePromise- Promise → async conversion
Protocol Support
@Awaitlessable- Generates sync method signatures for protocols with async methods@Awaitable- Generates async method signatures for protocols with Publisher/completion methods
#awaitless() - inline async code execution
Execute async code blocks synchronously in non-async contexts.
@IsolatedSafe - generate thread-safe properties
Automatic runtime thread-safe wrappers for nonisolated(unsafe) properties with configurable synchronization strategies.
Awaitless.run() - low-level bridge
Direct function for running async code in sync contexts with fine-grained control.
Configuration System - four-level configuration hierarchy
AwaitlessKit provides a flexible configuration system with multiple levels of precedence for customizing generated code behavior.
- Process-Level Defaults via
AwaitlessConfig.setDefaults() - Type-Scoped Configuration via
@AwaitlessConfigmember macro - Method-Level Configuration via
@Awaitlessparameters - Built-in Defaults as fallback
More Examples
Non-async Function
class APIService {
@Awaitless
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Generates: @available(*, noasync) func fetchData() throws -> Data
}Combine Publisher
@AwaitlessPublisher generates a publisher backed by a dedicated cancellation-aware task publisher. This provides:
- Correct cancellation propagation (cancelling the subscription cancels the underlying
Task) - Memory behavior (no retained promise closure beyond execution)
- Semantic clarity (single-shot mapping of async result to a Combine stream)
- Clear failure typing: non-throwing async ->
AnyPublisher<Output, Never>, throwing async ->AnyPublisher<Output, Error>
Throwing example:
class APIService {
@AwaitlessPublisher
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Generates: func fetchData() -> AnyPublisher<Data, Error>
}Non-throwing example:
class TimeService {
@AwaitlessPublisher
func currentTimestamp() async -> Int {
Int(Date().timeIntervalSince1970)
}
// Generates: func currentTimestamp() -> AnyPublisher<Int, Never>
}Main thread delivery:
class ProfileService {
@AwaitlessPublisher(deliverOn: .main)
func loadProfile(id: String) async throws -> Profile {
// ...
}
// Generated publisher delivers value & completion on DispatchQueue.main
}Under the hood the macro calls an internal factory that uses TaskThrowingPublisher / TaskPublisher (adapted from a Swift Forums discussion on correctly bridging async functions to Combine) to produce the AnyPublisher.
PromiseKit Integration
Bidirectional conversion between async/await and PromiseKit with @AwaitlessPromise and @Awaitable:
// Add PromiseKit integration to Package.swift
.product(name: "AwaitlessKit-PromiseKit", package: "AwaitlessKit")Async to Promise conversion:
import AwaitlessKit
import PromiseKit
class NetworkService {
@AwaitlessPromise(prefix: "promise_")
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Generates: func promise_fetchData() -> Promise<Data>
}
// Use with PromiseKit
service.promise_fetchData()
.done { data in print("Success: \(data)") }
.catch { error in print("Error: \(error)") }Promise to async conversion:
class LegacyService {
@Awaitable(prefix: "async_")
func legacyFetchData() -> Promise<Data> {
return URLSession.shared.dataTask(.promise, with: url).map(\.data)
}
// Generates:
// @available(*, deprecated: "PromiseKit support is deprecated; use async function instead")
// func async_legacyFetchData() async throws -> Data
}
// Use with async/await
let data = try await service.async_legacyFetchData()Perfect for gradual migration between PromiseKit and async/await architectures.
Completion Handler
class APIService {
@AwaitlessCompletion
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
// Generates: func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}Documentation
AwaitlessKit includes comprehensive DocC documentation with detailed guides, examples, and API reference.
Key Documentation Sections
- Usage Guide - Quick reference with practical examples and common patterns
- Examples Guide - Comprehensive examples from basic usage to advanced patterns
- PromiseKit Integration - Bidirectional conversion between async/await and PromiseKit
- Configuration System - Four-level configuration hierarchy and customization options
- Migration Guide - Step-by-step migration strategies and best practices
- Macro Implementation - Technical details for extending and contributing to AwaitlessKit
What You'll Find
- Quick Reference - Fast lookup for common macro usage patterns and configurations
- Real-world Examples - From simple async functions to complex migration scenarios
- PromiseKit Integration - Complete bidirectional conversion guide with migration strategies
- Configuration Patterns - Process-level, type-scoped, and method-level configuration examples
- Migration Strategies - Progressive deprecation, brownfield conversion, and testing approaches
- Best Practices - Naming conventions, error handling, and testing approaches
- Technical Details - Macro implementation, SwiftSyntax integration, and extension points
The documentation is designed to help you successfully adopt async/await in your projects while maintaining backward compatibility during the transition period.
Migration Overview
Phase 1: Add Async Code with autogenerated sync function
Implement async functions while maintaining synchronous compatibility for existing callers:
class DataManager {
// Autogenerate noasync version alongside new async function
@Awaitless
func loadData() async throws -> [String] {
// New async implementation
}
}Phase 2: Deprecate generated sync function
Add deprecation warnings to encourage migration to async versions while still providing the synchronous fallback:
class DataManager {
@Awaitless(.deprecated("Migrate to async version by Q2 2026"))
func loadData() async throws -> [String] {
// Implementation
}
}Phase 3: Make sync function unavailable
Force migration by making the synchronous version unavailable, providing clear error messages about required changes:
class DataManager {
@Awaitless(.unavailable("Sync version removed. Use async version only"))
func loadData() async throws -> [String] {
// Implementation
}
}Phase 4: Remove macro and autogenerated function
Complete the migration by removing the AwaitlessKit macro entirely, leaving only the pure async implementation:
class DataManager {
func loadData() async throws -> [String] {
// Pure async implementation
}
}License
MIT License. See LICENSE for details.
Credits
- Wade Tregaskis for
Task.noasyncfrom Calling Swift Concurrency async code synchronously in Swift - Zed Editor for its powerful agentic GenAI support
- Anthropic for Claude 3.7 and 4.0 models
Remember: AwaitlessKit is a migration tool, not a permanent solution. Plan your async/await adoption strategy and use this library to smooth the transition.
Package Metadata
Repository: bonkey/AwaitlessKit
Homepage: https://swiftpackageindex.com/bonkey/AwaitlessKit/main/documentation/awaitlesskitmacros
Stars: 6
Forks: 1
Open issues: 0
Default branch: main
Primary language: swift
License: MIT
Topics: structured-concurrency, swift, swift6
README: README.md