Skip to content

elf0-fr/ETBDependencyInjection

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ETBDependencyInjection

Compile-time Dependency Injection with Swift Macros

Overview

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 @Injection on properties.
  • Flexible resolution: Support for protocols and any existential 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.

Requirements

  • Swift 6.2 or later (the package uses swift-tools-version: 6.2)
  • Platforms:
    • macOS 14+
    • iOS 17+
    • tvOS 17+
    • watchOS 10+
    • macCatalyst 17+

Installation

Xcode

  1. In Xcode, go to File > Add Package Dependencies…
  2. Enter the package URL: https://github.com/elf0-fr/ETBDependencyInjection.git
  3. Add the ETBDependencyInjection product to your target.

Swift Package Manager (Package.swift)

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")
        ]
    )
]

Example

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

About

Dependency injection tool

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages