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.1.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. |
🚫 Opt-Out Macro (@SkipInit) |
Excludes a specific async method from automatic @WaitForInit stamping. |
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| I{"Has @SkipInit?"}
I -->|Yes| J["Skip (opted out) 🚫"]
I -->|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()
}
}When using @AutoAwaitInit or @AutoAwaitThrowingInit, every async method gets gated automatically. But sometimes you need a specific method to run before initialization completes — for example, the setup method itself, a cancellation handler, or a status check.
Mark those methods with @SkipInit to exclude them:
import Initializable
@AutoAwaitInit
actor NetworkService: Initializable {
let gate = InitializationGate()
private var session: URLSession?
// 🚫 Opted out — this IS the initialization method
@SkipInit
func bootstrap() async {
session = await createSession()
await markInitialized()
}
// 🚫 Opted out — must be callable anytime
@SkipInit
func status() async -> String {
return await initialized ? "ready" : "starting…"
}
// ✅ Auto-gated — waits for bootstrap()
func fetch(_ url: URL) async -> Data { ... }
func upload(_ data: Data) async { ... }
}Tip
@SkipInit only works inside types that have @AutoAwaitInit or @AutoAwaitThrowingInit. Applying it elsewhere emits a compiler error with a fix-it.
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, @SkipInit 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
├── SkipInitMacro.swift # @SkipInit peer macro implementation
├── Messages.swift # Diagnostic & fix-it messages
├── FunctionDeclSyntax+Extensions.swift # AST inspection helpers
└── MemberAttributeMacro+Extensions.swift # Duplicate & @SkipInit 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
├── SkipInitMacroTests.swift # @SkipInit peer macro 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, @SkipInit 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 |
| Feature | Details |
|---|---|
| Type | @attached(peer) |
| Target | Individual async function inside a type using @AutoAwaitInit or @AutoAwaitThrowingInit |
| Effect | Prevents the enclosing member-attribute macro from stamping @WaitForInit/@WaitForThrowingInit on this method |
| Produces | No peer declarations — acts purely as a compile-time marker |
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 |
| Scenario | Diagnostic Error Message | Xcode Fix-It Suggestion |
|---|---|---|
| Sync function | @SkipInit can only be applied to async functions, sync functions are never wrapped |
Remove @SkipInit |
No enclosing @AutoAwait* |
@SkipInit can only be used inside a type marked with @AutoAwaitInit or @AutoAwaitThrowingInit |
Remove @SkipInit |
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.1.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
