Compile-time-safe dependency injection in Swift
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.
[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
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.
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:
You can see sample integrations in the Examples folder. If you are migrating an existing project to SafeDI, follow our migration guide.
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.
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.
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.
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.
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.
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.
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!
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.
Special thanks to @kierajmumick for helping shape the early design of SafeDI.