Skip to content

pedro0x53/volt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Volt

A lightweight dependency injection package for Swift, built on static subscript storage and macros.

Concepts

Volt has four building blocks:

  • ChargerSchema — a protocol that marks a class as a DI container. Containers hold dependencies via static subscripts keyed by BatteryCell types.
  • @Charger — a macro that synthesizes ChargerSchema conformance and a public init() on a class.
  • @Charge — a macro applied to properties inside a ChargerSchema. 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.

Defining a container

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()
}

Consuming dependencies

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
}

Testing

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)
        // ...
    }
}

Custom container

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
}

Thread safety

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.

Requirements

  • Swift 6.0+
  • iOS 13+ / macOS 10.15+ / tvOS 13+ / watchOS 6+ / visionOS 1+

About

A lightweight dependency injection package for Swift, built on static subscript storage and macros.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages