SafeDI

Compile-time-safe dependency injection in Swift

112
3
Swift

SafeDI

CI Status
codecov
License

Compile-time-safe dependency injection for Swift projects. SafeDI provides developers with the safety and simplicity of manual dependency injection, without the overhead of boilerplate code.

Features

  • [x] Compile-time safe

  • [x] Thread safe

  • [x] Hierarchical dependency scoping

  • [x] Constructor injection

  • [x] Multi-module support

  • [x] Dependency inversion support

  • [x] Transitive dependency solving

  • [x] Cycle detection

  • [x] Architecture independent

  • [x] Simple integration: no DI-specific types or generics required

  • [x] Easy testing: every type has a memberwise initializer

  • [x] Clear error messages: never debug generated code

The core concept

SafeDI reads your code, validates your dependencies, and generates a dependency tree—all during project compilation. If your code compiles, your dependency tree is valid.

Opting a type into the SafeDI dependency tree is simple: add the @Instantiable macro to your type declaration, and decorate your type’s dependencies with macros to indicate the lifecycle of each property. Here is what a Boiler in a CoffeeMaker might look like in SafeDI:

// The boiler type is opted into SafeDI because it has been decorated with the `@Instantiable` macro.
@Instantiable
public final class Boiler {
    public init(pump: Pump, waterReservoir: WaterReservoir) {
        self.pump = pump
        self.waterReservoir = waterReservoir
    }

    …

    // The boiler creates—or in SafeDI parlance ‘instantiates’—its pump.
    @Instantiated private let pump: Pump
    // The boiler receives a reference to a water reservoir that has been instantiated by the coffee maker.
    @Received private let waterReservoir: WaterReservoir
}

That is all it takes! SafeDI utilizes macro decorations on your existing types to define your dependency tree. For a comprehensive explanation of SafeDI’s macros and their usage, please read the Macros section of our manual.

Getting started

SafeDI utilizes both Swift macros and a code generation plugin to read your code and generate a dependency tree. To integrate SafeDI, follow these three steps:

  1. Add SafeDI as a dependency to your project
  2. Integrate SafeDI’s code generation into your build
  3. Create your dependency tree using SafeDI’s macros

You can see sample integrations in the Examples folder. If you are migrating an existing project to SafeDI, follow our migration guide.

Adding SafeDI as a Dependency

To add the SafeDI framework as a dependency to a package utilizing Swift Package Manager, add the following lines to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/dfed/SafeDI.git", from: "1.0.0"),
]

To install the SafeDI framework into an Xcode project with Swift Package Manager, follow Apple’s instructions to add https://github.com/dfed/SafeDI.git as a dependency.

Generating your dependency tree

SafeDI provides a code generation plugin named SafeDIGenerator. This plugin works out of the box on a limited number of project configurations. If your project does not fall into these well-supported configurations, you can configure your build to utilize the SafeDITool command-line executable directly.

Single-module Xcode projects

If your first-party code comprises a single module in an .xcodeproj, once your Xcode project depends on the SafeDI package you can integrate the Swift Package Plugin simply by going to your target’s Build Phases, expanding the Run Build Tool Plug-ins drop-down, and adding the SafeDIGenerator as a build tool plug-in. You can see this integration in practice in the ExampleProjectIntegration project.

Swift package

If your first-party code is entirely contained in a Swift Package with one or more modules, you can add the following lines to your root target’s definition:

    plugins: [
        .plugin(name: "SafeDIGenerator", package: "SafeDI")
    ]

You can see this integration in practice in the ExamplePackageIntegration package.

Additional configurations

If your first-party code comprises multiple modules in Xcode, or a mix of Xcode Projects and Swift Packages, or some other configuration, once your Xcode project depends on the SafeDI package you will need to utilize the SafeDITool command-line executable directly in a pre-build script.

set -e

VERSION='<<VERSION>>'
DESTINATION="$BUILD_DIR/SafeDITool-Release/$VERSION/safeditool"

# Download the tool from Github releases.
if [ -f "$DESTINATION" ]; then
    if [ ! -x "$DESTINATION" ]; then
        chmod +x "$DESTINATION"
    fi
else
    mkdir -p "$(dirname "$DESTINATION")"

    ARCH=$(uname -m)
    if [ "$ARCH" = "arm64" ]; then
        ARCH_PATH="SafeDITool-arm64"
    elif [ "$ARCH" = "x86_64" ]; then
        ARCH_PATH="SafeDITool-x86_64"
    else
        echo "Unsupported architecture: $ARCH"
        exit 1
    fi
    curl -L -o "$DESTINATION" "https://github.com/dfed/SafeDI/releases/download/$VERSION/$ARCH_PATH"
    chmod +x "$DESTINATION"
fi

# Run the tool.
$DESTINATION --include "$PROJECT_DIR/<<RELATIVE_PATH_TO_SOURCE_FILES>>" "$PROJECT_DIR/<<RELATIVE_PATH_TO_MORE_SOURCE_FILES>>" --dependency-tree-output "$PROJECT_DIR/<<RELATIVE_PATH_TO_WRITE_OUTPUT_FILE>>"

Make sure to set ENABLE_USER_SCRIPT_SANDBOXING to NO in your target, and to replace the <<VERSION>>, <<RELATIVE_PATH_TO_SOURCE_FILES>>, <<RELATIVE_PATH_TO_MORE_SOURCE_FILES>>, and <<RELATIVE_PATH_TO_WRITE_OUTPUT_FILE>> with the appropriate values. Also ensure that you add $PROJECT_DIR/<<RELATIVE_PATH_TO_WRITE_OUTPUT_FILE>> to the build script’s Output Files list.

You can see this in integration in practice in the ExampleMultiProjectIntegration package.

SafeDITool is designed to integrate into projects of any size or shape.

SafeDITool can parse all of your Swift files at once, or for even better performance, the tool can be run on each dependent module as part of the build. Run swift run SafeDITool --help to see documentation of the tool’s supported arguments.

Comparing SafeDI to other DI libraries

SafeDI’s compile-time safety and hierarchical dependency scoping make it similar to Needle and Weaver. Unlike Needle, SafeDI does not require defining dependency protocols for each type that can be instantiated within the DI tree. Unlike Weaver, SafeDI does not require defining and maintaining containers that live alongside your regular Swift code.

Other Swift DI libraries, like Swinject and swift-dependencies, do not offer compile-time safety. Meanwhile, libraries like Factory do offer compile-time validation of the dependency tree, but prevent hierarchical dependency scoping. This means scoped dependencies—like an authentication token in a network layer—can only be optionally injected when using Factory.

Contributing

I’m glad you’re interested in SafeDI, and I’d love to see where you take it. Please review the contributing guidelines prior to submitting a Pull Request.

Thanks for being part of this journey, and happy injecting!

Author

SafeDI was created by Dan Federman, the architect of Airbnb’s closed-source Swift dependency injection system. Following his tenure at Airbnb, Dan developed SafeDI to share a modern, compile-time-safe dependency injection solution with the Swift community.

Dan has a proven track record of maintaining open-source libraries: he co-created Valet and has been maintaining the repo since its debut in 2015.

Acknowledgements

Special thanks to @kierajmumick for helping shape the early design of SafeDI.