Home

Awesome

MDI

Zero-cost dependency injection using Swift Macros

Features

Requirements

Limitations

Presently, the following limitations exist (due to compiler bug):

Versions

Version 4.2.0

Version 4.1.0

Version 4.0.0

Version 3.0.0

Version 2.2.0

Version 2.1.0

Version 2.0.2

Version 2.0.1

Version 2.0.0

Version 1.1.0

Example Usage

Plain types & existencials

Define some type that will serve both as the "assembly hub" and the resolution entry point. (As stated in Limitations, enums are not supported).

enum Dependency { }

On this example, assemblies will be defined in extensions of Dependency (although this is not mandatory and can be done directly on the type declaration). In the following example, we will assume the following types:

protocol ABTestingProtocol { }
protocol CodeGuardsProtocol { }
protocol ThemeProtocol { }

final class ABTesting: ABTestingProtocol { }
final class CodeGuards: CodeGuardsProtocol { }
final class Theme: ThemeProtocol {
    private let abTesting: ABTestingProtocol
    private let codeGuards: CodeGuardsProtocol

    init(
        abTesting: ABTestingProtocol,
        codeGuards: CodeGuardsProtocol
    ) {
        self.abTesting = abTesting
        self.codeGuards = codeGuards
    }
}

And then create an assembly to register them:

import MDI

@SingletonRegister((any ABTestingProtocol).self, using: ABTesting.init)
@SingletonRegister((any CodeGuardsProtocol).self, using: CodeGuards.init)
@SingletonRegister((any ThemeProtocol).self, parameterTypes: (any ABTestingProtocol).self, (any CodeGuardsProtocol).self, using: Theme.init(abTesting:codeGuards:))
extension Dependency { }

@SingletonRegister will call resolve on both ABTestingProtocol and CodeGuardsProtocol. Since both are declared in the assembly (mind they could easily be declared elsewhere) this succeeds; otherwise we'd get a compiler error. Note that, in the previous example, all dependencies were singletons, but this obviously did not have to be the case.

If instead @AutoRegister was used:

import MDI

@AutoRegister((any ABTestingProtocol).self, using: ABTesting.init)
@AutoRegister((any CodeGuardsProtocol).self, using: CodeGuards.init)
@AutoRegister((any ThemeProtocol).self, parameterTypes: (any ABTestingProtocol).self, (any CodeGuardsProtocol).self, using: Theme.init(abTesting:codeGuards:))
extension Dependency { }

New instances of the registered types would be created on each call to resolve(...).

Finally, some dependencies require parameters that cannot be resolved, but rather passed when instancing. This can easily be achieved via @FactoryRegister. In the following example, we can resolve ThemeProtocol, but not necessarily boot: Date or sessionId: String.

protocol AppContextProtocol {}

final class AppContext: AppContextProtocol {
    let boot: Date
    let sessionId: String
    let theme: ThemeProtocol

    init(
        boot: Date,
        sessionId: String,
        theme: ThemeProtocol
    ) {
        self.boot = boot
        self.sessionId = sessionId
        self.theme = theme
    }
}

Using @FactoryRegister we can expose the required parameters while even leveraging resolve(...) in the factory method to resolve ThemeProtocol:

import MDI

@FactoryRegister(
    (any AppContextProtocol).self,
    parameterTypes: .explicit(Date.self), .explicit(String.self), .resolved((any Theme).self),
    using: AppContext.init(boot:sessionId:theme:)
)
extension Dependency { }

This will expose a resolve method that exposes Date and String while implicitly resolving Theme.

extension Dependency {
    static func resolve(_: any AppContextProtocol, boot: Date, sessionId: String) -> any AppContextProtocol {
        return (AppContextProtocolImpl.init(boot:sessionId:theme:))(boot, sessionId, Self.resolve())
    }
}

Factories

All registered types (save for singletons) now expose factory methods/types. If a dependency that requires no parameters for resolution, a single factory(of: ...) method will be exposed. E.g. for the type:

@AutoRegister((any ABTestingProtocol).self, using: ABTesting.init)
extension Dependency { ... }

A single factory method will be exposed:

extension Dependency {
    ...
    static func factory(of _: (any ABTestingProtocol).Type) -> MDIFactory<any ABTestingProtocol> {
        return Dependency.resolve((any ABTestingProtocol).self)
    }
}

For opaque types, the main difference is that MDIFactory returns some ABTestingProtocol instead of the existencial. Whenever a dependency requires parameters for resolution, a factory type will be created and two factory methods will be exposed. For the following example:

@FactoryRegister(
    (any AppContextProtocol).self,
    parameterTypes: .explicit(Date.self), .explicit(String.self), .resolved((any Theme).self),
    using: AppContext.init(boot:sessionId:theme:)
)
extension Dependency { }

A type AppContextProtocolFactory will be declared, and two methods exposed:

extension Dependency {
    ...
    struct AppContextProtocolFactory {
        fileprivate init() {
        
        }

        public func make(boot: Date, sessionId: String) -> any AppContextProtocol {
            return Dependency.resolve((any AppContextProtocol).self, boot: boot, sessionId: sessionId)
        }
    }

    static func factory(of: (any AppContextProtocol).Type) -> AppContextProtocolFactory {
        return AppContextProtocolFactory()
    }

    static func factory(of: (any AppContextProtocol).Type, boot: Date, sessionId: String) -> MDIFactory<any AppContextProtocol> {
        return MDIFactory {
            MDIDependency.resolve((any AppContextProtocol).self, boot: boot, sessionId: sessionId)
        }
    }
}

Opaque types

Variants for the previous macros exist, supporting opaque types:

The main differences being:

Installation

Swift Package Manager

You can use the Swift Package Manager to install your package by adding it as a dependency to your Package.swift file:

dependencies: [
    .package(url: "git@github.com:renato-iar/MDI.git", from: "1.0.0")
]