A lightweight dependency injection package for Swift, built on static subscript storage and macros.
Volt has four building blocks:
ChargerSchema— a protocol that marks a class as a DI container. Containers hold dependencies via static subscripts keyed byBatteryCelltypes.@Charger— a macro that synthesizesChargerSchemaconformance and apublic init()on a class.@Charge— a macro applied to properties inside aChargerSchema. It generates the key struct and the get/set accessors automatically.@Discharge— a property wrapper applied to properties in workers, services, mediators, or facades. It resolves the dependency from the container at the point of access.
Use @Charger on a class and declare dependencies with @Charge:
@Charger
final class AppContainer {
@Charge var apiClient: APIClient = DefaultAPIClient()
@Charge var analytics: Analytics = DefaultAnalytics()
}Alternatively, use the built-in Battery class to extend a shared default container:
extension Battery {
@Charge var apiClient: APIClient = DefaultAPIClient()
}Apply @Discharge to properties in any type that needs a dependency — services, workers, mediators, facades.
When using Battery, pass the key path directly:
final class ProfileService {
@Discharge(\.apiClient) var apiClient: APIClient
}When using a custom container, pass the container type explicitly:
final class OrderMediator {
@Discharge(AppContainer.self, \.apiClient) var apiClient: APIClient
@Discharge(AppContainer.self, \.analytics) var analytics: Analytics
}Use the charge and discharge static methods to set up and tear down dependencies inside tests:
final class ProfileServiceTests {
func setUp() {
AppContainer.charge(\.apiClient, MockAPIClient())
}
func tearDown() {
AppContainer.charge(\.apiClient, DefaultAPIClient())
}
func testFetchProfile() {
let analytics = AppContainer.discharge(\.analytics)
// ...
}
}Volt's types are designed to be wrapped or aliased to match your app's naming conventions. A typical setup involves three things: a container, a registration macro, and a resolver.
import Volt
// Container — alternative to Battery
@Charger
public final class CustomContainer {}
// Registration macro — alternative to @Charge
@attached(accessor)
@attached(peer, names: arbitrary)
public macro Inlet() = #externalMacro(module: "VoltMacros", type: "ChargeMacro")
// Resolver — alternative to @Discharge
public typealias Outlet<Value> = Discharge<CustomContainer, Value>
public extension Discharge {
init(_ keyPath: KeyPath<CustomContainer, Value>) where Charger == CustomContainer {
self.init(CustomContainer.self, keyPath)
}
}With this in place, the usage reads entirely in your own vocabulary:
// Registering
extension CustomContainer {
@Inlet var apiClient: APIClient = DefaultAPIClient()
}
// Consuming
final class ProfileService {
@Outlet(\.apiClient) var apiClient: APIClient
}All reads and writes go through a shared concurrent DispatchQueue. Reads are synchronous, writes use a barrier — so the container is safe to use from multiple threads without additional locking.
- Swift 6.0+
- iOS 13+ / macOS 10.15+ / tvOS 13+ / watchOS 6+ / visionOS 1+