Compile-time Dependency Injection with Swift Macros
ETBDependencyInjection is a Swift Package that provides a Swift macro-driven approach to dependency injection. It eliminates boilerplate for wiring services into your types while keeping zero runtime cost. Use the @Injection property wrapper and accompanying macros to declare dependencies declaratively; the macro generates the necessary accessors at compile time.
- Zero runtime overhead: All code is generated by the macro at compile time.
- Declarative and concise: Express dependencies with
@Injectionon properties. - Flexible resolution: Support for protocols and
anyexistential types. - Non-invasive: If a property is already initialized or overridden, the macro does nothing.
- Testable by design: Swap providers or register test doubles in your tests.
- Swift 6.2 or later (the package uses swift-tools-version: 6.2)
- Platforms:
- macOS 14+
- iOS 17+
- tvOS 17+
- watchOS 10+
- macCatalyst 17+
- In Xcode, go to File > Add Package Dependencies…
- Enter the package URL:
https://github.com/elf0-fr/ETBDependencyInjection.git - Add the
ETBDependencyInjectionproduct to your target.
Add the package to your dependencies and link the ETBDependencyInjection product to your target:
// In Package.swift
dependencies: [
.package(url: "https://github.com/elf0-fr/ETBDependencyInjection.git", from: "1.0.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "ETBDependencyInjection", package: "ETBDependencyInjection")
]
)
]Add a concrete implementation for the protocols
public final class ContainerImp: Container {
// Your implementation or delegation to a DI framework
}
public final class ServiceCollectionImp: ServiceCollection {
// Your implementation or delegation to a DI framework
}
public final class ServiceProviderImp: ServiceProvider {
// Your implementation or delegation to a DI framework
}Declare a service contract
protocol MyService {
func doWork() -> String
}Apply the Service macro to a service you want to inject in your dependency injection system.
@Service(MyService.self)
class MyServiceImp: MyService {
// Resolve dependency within a service
@Injection var anotherService: any AnotherService
// Add your MyService protocol conformance:
func doWork() -> String { anotherService.doAnotherWork() }
}
// Here is the expanded version
class MyServiceImp: MyService {
// To simplify I did not expand the Injection macro here.
@Injection var anotherService: any AnotherService
typealias Interface = any MyService
var provider: (any ServiceProvider)?
required init(provider: any ServiceProvider) {
self.provider = provider
}
init(anotherService: any AnotherService) {
self.anotherService = anotherService
}
func doWork() -> String { anotherService.doAnotherWork() }
}Create the container, register the services, build and share the provider with the views (example API)
@main
struct YourApp: App {
@State private var provider: ServiceProvider
init() {
// Create the container
let container = ContainerImp()
// Register your services
_ = try? container.services.register(as: MyServiceImp.self)
// Build to create the provider
container.build()
do {
_provider = .init(wrappedValue: try container.provider)
} catch {
// handle error
}
}
var body: some Scene {
YourView(provider: provider)
}
}Consume the dependency. Apply the Injectable macro to an entity that will consume your dependencies but will not be part of it.
@Injectable
class ViewModel {
// Resolve dependency
@Injection var service: any MyService
var provider: (any ServiceProvider)?
// Unlike Service, Injectable does not require a init(provider:) initializer.
// You are free to initialize self.provider as you like.
init(provider: any ServiceProvider) {
self.provider = provider
}
// Manualy inject dependency is also possible
init(service: any MyService) {
self.service = service
}
func run() {
print(service.doWork())
}
}
// Special case for Observation
@Injectable
@Observable
class ViewModel {
// add @ObservationIgnored after @Injection
@Injection @ObservationIgnored var service: any MyService
// ...
}The Injection macro
class MyClass {
@Injection var service: any MyService
var provider: (any ServiceProvider)?
}
// Here is the expanded version
class MyClass {
var service: any MyService {
get {
if _injection_service == nil {
_injection_service = provider?.resolveRequired((any MyService).self)
}
if let _injection_service {
return _injection_service
} else {
fatalError()
}
}
set {
_injection_service = newValue
}
}
private var _injection_service: (any MyService)?
var provider: (any ServiceProvider)?
}