Zero-Boilerplate Async Initialization Gating for Swift Actors, Classes, and Structs
Stop scattering guard isReady checks everywhere. Let the Swift compiler enforce initialization gates for you using the power of Macros.
Note
Initializable guarantees that your type's async methods will automatically suspend until asynchronous setup is completely finished. No more runtime crashes due to uninitialized state. It works seamlessly with actor, class, and struct.
Types like actors or classes often require asynchronous setup before they are ready to be usedβconnecting to a database, loading a configuration file, or authenticating with a remote server.
Every method that depends on this setup must somehow wait until it's done.
class DatabaseService {
private var isReady = false
func query(_ sql: String) async -> [Row] {
// π© You have to remember this everywhere
while !isReady { await Task.yield() }
return try await db.execute(sql)
}
func insert(_ row: Row) async {
// π© Miss one and you get a runtime crash
while !isReady { await Task.yield() }
db.insert(row)
}
}This is tedious, highly error-prone, and doesn't scale as your codebase grows.
Initializable gives you a single, elegant annotation on your type. Every async method automatically waits for initialization to complete!
@AutoAwaitInit
class DatabaseService: Initializable {
let gate = InitializationGate()
func setup() async {
await connectToDatabase()
// π Gate opens β all waiting methods proceed!
await markInitialized()
}
// β
Automatically waits for setup() β ZERO boilerplate needed!
func query(_ sql: String) async -> [Row] { ... }
func insert(_ row: Row) async { ... }
func delete(_ id: Int) async { ... }
}Tip
Zero runtime overhead after initialization. Zero boilerplate. Zero chance of forgetting a check.
- Quick Start
- Core Concepts
- Usage Guide
- Architecture
- Macro Reference
- Diagnostics & Fix-Its
- API Reference
- Installation
- Requirements
In your Package.swift, add the dependency:
dependencies: [
.package(url: "https://github.com/k-arindam/Initializable.git", from: "1.0.0")
]Import the module and annotate your type (can be actor, class, or struct):
import Initializable
@AutoAwaitInit
actor MyService: Initializable {
let gate = InitializationGate()
func setup() async {
// ... perform your async setup ...
await markInitialized()
}
func fetchData() async -> Data {
// β¨ MAGIC: `await awaitInitialized()` is injected here by the macro
return cachedData
}
}Simply call your methods. They will automatically wait if setup isn't finished!
let service = MyService()
// Kick off the setup (it will run concurrently)
Task { await service.setup() }
// This call will safely suspend until `setup()` completes!
let data = await service.fetchData() At a high level, Initializable uses Swift Macros to inject gating logic at compile time, and an Actor-based state machine to manage continuations at runtime.
graph LR
subgraph "π Compile Time"
A["@AutoAwaitInit"] -->|stamps| B["@WaitForInit"]
B -->|injects| C["await awaitInitialized()"]
end
subgraph "πββοΈ Runtime"
D["InitializationGate"] -->|pending| E["Callers suspend"]
D -->|markInitialized| F["Callers resume"]
end
C -.->|calls| D
| Concept | Description |
|---|---|
π Protocol (Initializable) |
Requires a gate property. Provides markInitialized(), awaitInitialized(), and initialized. |
π§ Gate (InitializationGate) |
Actor that safely holds continuations and resumes them when the gate opens. |
π Body Macro (@WaitForInit) |
Injects await awaitInitialized() at the start of a single specific method. |
π·οΈ Member Macro (@AutoAwaitInit) |
Automatically stamps @WaitForInit on all async methods in the type. |
There are throwing variants of each component for failable initialization (e.g. network requests that might fail):
| Component Type | Non-Throwing | Throwing (Failable) |
|---|---|---|
| Protocol | Initializable |
ThrowingInitializable |
| Gate | InitializationGate |
ThrowingInitializationGate |
| Body Macro | @WaitForInit |
@WaitForThrowingInit |
| Member Macro | @AutoAwaitInit |
@AutoAwaitThrowingInit |
Use this when your setup cannot fail (e.g., loading a local cache, connecting to an in-memory store).
import Initializable
@AutoAwaitInit
class CacheService: Initializable {
let gate = InitializationGate()
private var store: [String: Data] = [:]
func warmUp() async {
store = await loadFromDisk()
await markInitialized()
}
// β
Auto-gated β automatically waits for warmUp()
func get(_ key: String) async -> Data? {
return store[key]
}
// β Sync β skipped by the macro (no gate needed)
func cacheDirectory() -> URL {
FileManager.default.temporaryDirectory
}
}flowchart TD
A["@AutoAwaitInit scans members"] --> B{"Is it a function?"}
B -->|No| C["Skip (property/init)"]
B -->|Yes| D{"Is it async?"}
D -->|No| E["Skip (sync method)"]
D -->|Yes| F{"Is it a protocol method?"}
F -->|"markInitialized / awaitInitialized"| G["Skip (excluded)"]
F -->|No| H["Stamp @WaitForInit β
"]
sequenceDiagram
participant Caller1
participant Caller2
participant Service
participant Gate
Caller1->>Service: get("key")
Service->>Gate: awaitInitialized()
Note over Gate: State: pending β suspend
Caller2->>Service: set("key", data)
Service->>Gate: awaitInitialized()
Note over Gate: State: pending β suspend
Service->>Gate: markInitialized()
Note over Gate: State: initialized
Gate-->>Caller1: resume β
Gate-->>Caller2: resume β
Note over Gate: Future calls return immediately
Use this when your setup can fail (e.g., network connections, database migrations, API authentication).
Important
You must use ThrowingInitializable, ThrowingInitializationGate, and @AutoAwaitThrowingInit.
import Initializable
@AutoAwaitThrowingInit
struct DatabaseService: ThrowingInitializable {
let gate = ThrowingInitializationGate()
private var connection: DBConnection?
mutating func connect(to url: URL) async {
do {
connection = try await DBConnection.open(url)
await markInitialized() // β
Success
} catch {
await markFailed(error) // β Propagate error to all waiting methods
}
}
// β
Auto-gated β waits for connect, or throws if connect failed
func query(_ sql: String) async throws -> [Row] {
return try await connection!.execute(sql)
}
// β οΈ WARNING: If a method is async but NOT throws, the macro will emit a compiler diagnostic with a fix-it!
// func ping() async -> Bool { ... }
}stateDiagram-v2
[*] --> Pending
Pending --> Initialized : markInitialized()
Pending --> Failed : markFailed(error)
Initialized --> Initialized : markInitialized() [no-op]
Initialized --> Initialized : markFailed() [no-op]
Failed --> Failed : markFailed() [no-op]
Failed --> Failed : markInitialized() [no-op]
note right of Initialized : awaitInitialized() β returns immediately
note right of Failed : awaitInitialized() β throws stored error
note right of Pending : awaitInitialized() β suspends
State Stickiness: The first call to
markInitialized()ormarkFailed(_:)wins. Subsequent calls to either method are safe no-ops.
If you prefer fine-grained control instead of the blanket @AutoAwaitInit macro, you can apply @WaitForInit to individual methods manually:
actor SelectiveService: Initializable {
let gate = InitializationGate()
func setup() async { await markInitialized() }
@WaitForInit // β Only this method will wait
func criticalOperation() async -> Result {
return performWork()
}
// No macro β caller is entirely responsible for timing
func bestEffortOperation() async -> Result? {
return try? performWork()
}
}Initializable is split into the runtime library and the compile-time macro plugin.
graph TB
subgraph "Your App"
App["App Code"]
end
subgraph "π¦ Initializable Module"
Proto["Initializable Protocol<br/>ThrowingInitializable Protocol"]
Gate["InitializationGate<br/>ThrowingInitializationGate"]
Macros["Macro Declarations<br/>@AutoAwaitInit, @WaitForInit, etc."]
end
subgraph "π InitializableMacros Module (Compiler Plugin)"
MacroImpl["AutoAwaitInitMacro<br/>WaitForInitMacro"]
Diag["Diagnostics & Fix-Its"]
Helpers["Syntax Helpers"]
end
App -->|"import Initializable"| Proto
App -->|"uses"| Gate
App -->|"@AutoAwaitInit"| Macros
Macros -->|"#externalMacro"| MacroImpl
MacroImpl --> Diag
MacroImpl --> Helpers
style App fill:#2d2d2d,stroke:#888,color:#fff
style Proto fill:#1a5276,stroke:#2980b9,color:#fff
style Gate fill:#1a5276,stroke:#2980b9,color:#fff
style Macros fill:#1a5276,stroke:#2980b9,color:#fff
style MacroImpl fill:#4a235a,stroke:#8e44ad,color:#fff
style Diag fill:#4a235a,stroke:#8e44ad,color:#fff
style Helpers fill:#4a235a,stroke:#8e44ad,color:#fff
Sources/
βββ Initializable/ # Public API
β βββ Enums.swift # InitializationState, GateType
β βββ Gate.swift # InitializationGate, ThrowingInitializationGate
β βββ Initializable.swift # Initializable, ThrowingInitializable protocols
β βββ Macros.swift # @AutoAwaitInit, @WaitForInit declarations
β
βββ InitializableMacros/ # Compiler plugin (not shipped in binary)
βββ InitializableMacros.swift # @main plugin entry point
βββ AutoAwaitInitMacro.swift # Member-attribute macro implementations
βββ WaitForInitMacro.swift # Body macro implementations
βββ Messages.swift # Diagnostic & fix-it messages
βββ FunctionDeclSyntax+Extensions.swift # AST inspection helpers
βββ MemberAttributeMacro+Extensions.swift # Duplicate detection logic
Tests/
βββ InitializableTests/
βββ WaitForInitMacroTests.swift # @WaitForInit body macro tests
βββ WaitForThrowingInitMacroTests.swift # @WaitForThrowingInit body macro tests
βββ AutoAwaitInitMacroTests.swift # @AutoAwaitInit member-attribute tests
βββ AutoAwaitThrowingInitMacroTests.swift # @AutoAwaitThrowingInit tests
βββ RuntimeTests.swift # Gate & protocol runtime behavior tests
| Feature | Details |
|---|---|
| Type | @attached(memberAttribute) |
| Target | Actor / Class / Struct conforming to Initializable (or ThrowingInitializable) |
| Effect | Stamps @WaitForInit (or @WaitForThrowingInit) on every qualifying async method |
| Excludes | markInitialized(), awaitInitialized(), markFailed(), non-function members, sync methods |
| Feature | Details |
|---|---|
| Type | @attached(body) |
| Target | Individual async (or async throws) function inside a conforming type |
| Effect | Prepends await awaitInitialized() (or try await...) to the function body |
Initializable provides rich compiler diagnostics with actionable fix-its. You'll never be left guessing what went wrong!
| Scenario | Diagnostic Error Message | Xcode Fix-It Suggestion |
|---|---|---|
| Sync function | @WaitForInit requires the function to be 'async' |
Add async |
throws-only function |
@WaitForThrowingInit requires the function to be 'async' |
Add async |
async-only function |
@WaitForThrowingInit requires the function to be 'throws' |
Add throws |
| Sync non-throwing | @WaitForThrowingInit requires the function to be 'async throws' |
Add async throws |
| No conformance | @WaitForInit can only be used in a type that conforms to 'Initializable' |
None |
| Free function | @WaitForInit can only be applied inside a type declaration |
None |
| Scenario | Diagnostic Error Message | Xcode Fix-It Suggestion |
|---|---|---|
| No conformance | @AutoAwaitInit can only be applied to a type that conforms to 'Initializable' |
None |
| Duplicate attribute | @WaitForInit should not be added manually when @AutoAwaitInit is applied... |
Remove @WaitForInit |
public protocol Initializable {
var gate: InitializationGate { get }
}initialized: Async boolean property. ReturnstrueaftermarkInitialized().markInitialized(): Opens the gate. Safe to call multiple times (idempotent).awaitInitialized(): Suspends execution until the gate is opened.
public protocol ThrowingInitializable {
var gate: ThrowingInitializationGate { get }
}initialized: Async boolean property. Returnstrueonly on success.markFailed<E: Error>(_ error: E): Fails the gate with the given error. Idempotent.awaitInitialized() throws: Suspends until resolved; throws if initialization failed.
- Continuation type:
CheckedContinuation<Void, Never> - Cancellation: Resumes normally (returns
Void). Task cancellation will not throw. - Thread safety: Actor-isolated β all state mutations are serial.
- Continuation type:
CheckedContinuation<Void, any Error> - Cancellation: Throws
CancellationErrorautomatically if the waiting task is cancelled. - State stickiness: The first resolution (success or failure) permanently locks the state.
Add the dependency to your Package.swift:
dependencies: [
.package(url: "https://github.com/k-arindam/Initializable.git", from: "1.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["Initializable"]
)
]Or via Xcode: File β Add Package Dependencies β paste the repository URL.
| Platform/Tool | Minimum Version |
|---|---|
| Swift | 6.3 |
| Xcode | 16.3 |
| iOS | 15.0 |
| macOS | 12.0 |
| tvOS | 15.0 |
| watchOS | 9.0 |
Note
Swift macros generally require Swift 5.9+, but this package leverages advanced Swift 6.3 features including @attached(body) macros and CheckedContinuation isolation.
This project is available under the MIT License. See the LICENSE file for details.
Built with β€οΈ using Swift Macros