A Swift Macro-based Dependency Injection library for clean, type-safe DI containers.
InnoDI is a static dependency graph and scope validation framework.
- Use
DIScopeto describe construction lifetime. - Use DAG validation and diagnostics to catch graph problems early.
- Do not treat container resolution as a runtime state machine.
Across the InnoSquad stack, runtime state transitions belong in InnoFlow,
navigation transitions belong in InnoRouter, and transport/session lifecycle
belongs in InnoNetwork.
- Compile-time safety: Macro-based validation catches errors at build time
- Zero boilerplate: Auto-generated initializers with optional override parameters
- Multiple scopes:
shared,input, andtransientlifecycle management - AutoWiring: Simplified syntax with
Type.selfandwith:dependencies - Strict name-based resolution: Factory parameters and
with:dependencies resolve by member name only - Init Override: Direct mock injection via init parameters (no separate Overrides struct)
- Protocol-first design: Encourage DIP compliance with
concreteopt-in
Add InnoDI to your Package.swift:
dependencies: [
.package(url: "https://github.com/InnoSquadCorp/InnoDI.git", from: "3.0.1")
]Then add it to your target:
.target(
name: "YourApp",
dependencies: ["InnoDI"]
)import InnoDI
protocol APIClientProtocol {
func fetch() async throws -> Data
}
struct APIClient: APIClientProtocol {
let baseURL: String
func fetch() async throws -> Data { /* ... */ }
}
@DIContainer
struct AppContainer {
@Provide(.input)
var baseURL: String
@Provide(.shared, APIClient.self, with: [\.baseURL])
var apiClient: any APIClientProtocol
}
// Usage
let container = AppContainer(baseURL: "https://api.example.com")
let client = container.apiClientFor more control, use factory closures instead:
@Provide(.shared, factory: { (baseURL: String) in
APIClient(baseURL: baseURL, timeout: 30)
})
var apiClient: any APIClientProtocolIf you are new to InnoDI, read the docs in this order:
- This README for installation, container syntax, and the supported model.
- Validation for local/build/global validation and observability artifacts.
- PolicyBoundaries for exact matching rules, exclusions, and fallback behavior.
- ModuleWideInitDetection for the custom
initrestriction model.
Release and maintenance references:
Marks a struct as a DI container. Generates init(...) with optional override parameters.
@DIContainer does not support user-defined init declarations in the annotated type or any extension.
Macro validation rejects body and same-file extension init declarations, and the build plugin extends the same rule to cross-file extensions. Boundary details such as generic/constrained exclusions and conservative fallback rules are documented in PolicyBoundaries.
Use the synthesized initializer, or remove the macro and wire the type manually.
@DIContainer(validate: Bool = true, root: Bool = false, validateDAG: Bool = true, mainActor: Bool = false)| Parameter | Default | Description |
|---|---|---|
validate |
true |
Reserved compatibility flag. Core construction invariants such as .shared/.transient factory requirements, .input restrictions, and concrete: true opt-in remain compile-time enforced. |
root |
false |
Mark container as root in graph rendering. |
validateDAG |
true |
Enable local/global DAG validation for this container. Set false to opt out from DAG checks. |
mainActor |
false |
Apply @MainActor isolation to generated initializer/accessors. |
Declares a dependency with its scope and factory.
@Provide(_ scope: DIScope = .shared, _ type: Type.self? = nil, with: [KeyPath] = [], factory: Any? = nil, asyncFactory: Any? = nil, concrete: Bool = false)| Parameter | Default | Description |
|---|---|---|
scope |
.shared |
Lifecycle scope (see below) |
type |
nil |
Concrete type for AutoWiring (alternative to factory) |
with |
[] |
Dependencies to inject via AutoWiring |
factory |
nil |
Factory expression (required for .shared and .transient if no type) |
asyncFactory |
nil |
Async factory closure (mutually exclusive with factory) |
concrete |
false |
Required opt-in when the dependency property type is concrete (see DIP section) |
| Scope | Description | Factory Required |
|---|---|---|
.input |
Provided at container initialization | No |
.shared |
Created once, cached for container lifetime | Yes |
.transient |
New instance created on every access | Yes |
Scope laws:
.inputis external data and must come from container initialization..sharedis stable for one container instance and should be reused on repeated access..transientmust produce fresh instances and should not be treated as cached state.
Use asyncFactory when construction is asynchronous:
@Provide(.shared, asyncFactory: { (config: AppConfig) async throws in
try await APIClient.make(config: config)
})
var apiClient: any APIClientProtocolRules:
factoryandasyncFactorycannot be used together..inputscope does not allowasyncFactory.asyncFactorymust be declared as anasyncclosure.
Use for values that must be provided when creating the container:
@DIContainer
struct AppContainer {
@Provide(.input)
var config: AppConfig
@Provide(.input)
var analytics: AnalyticsService
}
let container = AppContainer(
config: AppConfig(env: .production),
analytics: FirebaseAnalytics()
)Use for services that should be instantiated once and reused:
@DIContainer
struct AppContainer {
@Provide(.shared, factory: URLSession.shared)
var session: any URLSessionProtocol
@Provide(.shared, factory: NetworkService(session: session))
var networkService: any NetworkServiceProtocol
}Use for objects that need a new instance on each access (e.g., ViewModels):
@DIContainer
struct AppContainer {
@Provide(.input)
var apiClient: any APIClientProtocol
@Provide(.transient, factory: HomeViewModel(api: apiClient))
var homeViewModel: HomeViewModel
@Provide(.transient, factory: ProfileViewModel(api: apiClient))
var profileViewModel: ProfileViewModel
}
// Each access creates a new instance
let vm1 = container.homeViewModel // New instance
let vm2 = container.homeViewModel // Another new instanceFor simpler cases, use Type.self with with: instead of verbose factory closures:
@DIContainer
struct AppContainer {
@Provide(.input)
var config: AppConfig
@Provide(.input)
var logger: Logger
// AutoWiring: APIClient(config: self.config, logger: self.logger)
@Provide(.shared, APIClient.self, with: [\.config, \.logger])
var apiClient: any APIClientProtocol
}Requirements:
- The property names in
with:must match the init parameter names of the concrete type - Example:
APIClient(config:logger:)matcheswith: [\.config, \.logger]
When to use factory closure instead:
- Parameter names don't match property names
- Complex initialization logic needed
- Need to transform dependencies
// Factory closure for complex cases
@Provide(.shared, factory: { (config: AppConfig) in
APIClient(configuration: config, timeout: 30)
})
var apiClient: any APIClientProtocolInnoDI enforces protocol-first dependencies for .shared and .transient.
Use explicit existential syntax (any Protocol) for protocol-typed dependencies.
If you need to use a concrete type, explicitly opt-in with concrete: true:
@DIContainer
struct AppContainer {
// Preferred: Protocol type
@Provide(.shared, factory: APIClient())
var apiClient: any APIClientProtocol
// Allowed: Concrete type with explicit opt-in
@Provide(.shared, factory: URLSession.shared, concrete: true)
var session: URLSession
}This makes concrete type usage intentional and visible in code review.
The generated init accepts optional parameters for .shared and .transient dependencies, allowing direct mock injection:
@DIContainer
struct AppContainer {
@Provide(.input)
var baseURL: String
@Provide(.shared, factory: APIClient(baseURL: baseURL))
var apiClient: any APIClientProtocol
}
// Production - factory creates the instance
let container = AppContainer(baseURL: "https://api.example.com")
// Testing - directly inject mock
let testContainer = AppContainer(
baseURL: "https://test.example.com",
apiClient: MockAPIClient() // Override with mock!
)Generated init signature:
init(baseURL: String, apiClient: (any APIClientProtocol)? = nil).inputparameters are required.sharedand.transientparameters are optional withnildefault- When
nil, the factory creates the instance; when provided, uses the injected value
InnoDI includes a command-line tool to generate dependency graphs from your @DIContainer declarations. This helps visualize the relationships between containers and their dependencies.
The CLI tool is included when you add InnoDI to your project. You can run it via Swift Package Manager:
swift run InnoDI-DependencyGraph --helpGenerate a Mermaid diagram (default):
swift run InnoDI-DependencyGraph --root /path/to/your/projectGenerate a DOT file for Graphviz:
swift run InnoDI-DependencyGraph --root /path/to/your/project --format dot --output graph.dotGenerate a PNG image directly (requires Graphviz installed):
swift run InnoDI-DependencyGraph --root /path/to/your/project --format dot --output graph.pngValidate global DAG (fails on cycle and ambiguous container references):
swift run InnoDI-DependencyGraph --root /path/to/your/project --validate-dag--root <path>: Root directory of the project (default: current directory)--format <mermaid|dot|ascii>: Output format (default: mermaid)--output <file>: Output file path (default: stdout)--validate-dag: Validate global container DAG and fail on cycle/ambiguity
- Containers annotated with
@DIContainer(validateDAG: false)are fully excluded from global DAG validation (--validate-dag), including cycle and ambiguity checks. - Macro-level dependency extraction for cycle validation is AST-based, so string literal tokens no longer produce false-positive dependency edges.
Generate local DocC docs:
Tools/generate-docc.shOnline DocC (GitHub Pages):
CI behavior from .github/workflows/docs.yml:
pull_request: uploadsinnodi-doccartifact for preview/download.pushtomain: deploys DocC site to GitHub Pages.
If this is your first Pages deployment, set repository Pages source to GitHub Actions
in repository settings.
InnoDI ships a SwiftPM build tool plugin:
InnoDIDAGValidationPlugin
The plugin coordinates validation once per package input state and reuses the shared result across targets, instead of rescanning the package graph independently for every target.
Attach it to your app target to fail builds when DAG validation fails:
.target(
name: "YourApp",
dependencies: ["InnoDI"],
plugins: [
.plugin(name: "InnoDIDAGValidationPlugin", package: "InnoDI")
]
)See runnable examples in /Examples:
/Examples/SwiftUIExample- a single feature root demonstrates navigation, loading skeletons, recoverable error/retry flow, and cancellation around local@Observablestate/Examples/TCAIntegrationExample/Examples/PreviewInjectionExample- live, preview, and failure roots render a richer preview matrix by swapping multiple services at the environment boundary/Examples/SampleApp
graph TD
AppContainer[root]
RepositoryContainer
UseCaseContainer
RemoteDataSourceContainer
FeatureContainer
ThirdPartyContainer
CoreContainer
AppContainer -->|loginBuilder| FeatureContainer
AppContainer --> RemoteDataSourceContainer
Use the included script to detect macro test performance regressions:
Tools/measure-macro-performance.shRun benchmark suites (10/50/100/250 dependencies):
Benchmarks/run-compile-bench.sh
Benchmarks/run-runtime-bench.sh
Benchmarks/compare.shOutput JSON files:
Benchmarks/results/compile.jsonBenchmarks/results/runtime.jsonBenchmarks/results/compare.json
Needle/SafeDI sections are currently scaffolded as non-blocking comparison slots in the report.
Update baseline after intentional performance changes:
Tools/measure-macro-performance.sh --iterations 5 --update-baselineDefault baseline file:
Tools/macro-performance-baseline.json
MIT
See LICENSE.