Swift-blade is a macro powered dependency injection framework for Swift.
It is heavily inspired by Dagger.
Declare swift-blade as a dependency in your Package.swift file:
.package(url: "https://github.com/shackley/swift-blade", from: "0.2.0")Add Blade as a dependency to your target(s):
dependencies: [
.product(name: "Blade", package: "swift-blade")
]We’ll demonstrate dependency injection with swift-blade by building a simple coffee maker. For complete sample code that you can compile and run, see swift-blade-example.
We'll start by defining a few types for our coffee maker.
Swift-blade utilizes initializers for dependency injection, so we'll start by writing our classes just like we would if we weren't using a dependency injection framework.
protocol Heater {}
protocol Pump {}
class ElectricHeater: Heater {
init() {}
}
class Thermosiphon: Pump {
private let heater: Heater
init(heater: Heater) {
self.heater = heater
}
}
class CoffeeMaker {
private let heater: Heater
private let pump: Pump
init(heater: Heater, pump: Pump) {
self.heater = heater
self.pump = pump
}
}Swift-blade takes care of initializing instances of your application's classes and providing their dependencies.
In order to do so, swift-blade must be told how to obtain instances of each type that it encounters. This is where the @Provider attribute comes in.
Let's go back in and add the @Provider attribute to the initializers of our classes so that swift-blade knows how to obtain instances of them.”
class ElectricHeater: Heater {
@Provider
init() {}
}
class Thermosiphon: Pump {
private let heater: Heater
@Provider
init(heater: Heater) {
self.heater = heater
}
}
class CoffeeMaker {
private let heater: Heater
private let pump: Pump
@Provider
init(heater: Heater, pump: Pump) {
self.heater = heater
self.pump = pump
}
}Now swift-blade can obtain instances of type ElectricHeater, Thermosiphon, and CoffeeMaker. However, it doesn't know how to satisfy dependencies of type Heater and Pump yet.
Since Heater and Pump are protocol types that can't be directly initialized, we'll define static @Provider functions that declare how dependencies of these types should be satisfied.
Static @Provider functions can have dependencies of their own. Since swift-blade knows how to obtain instances of type ElectricHeater and Thermosiphon, the providers for Heater and Pump can be written as:
@Provider
static func provideHeater(heater: ElectricHeater) -> Heater {
heater
}
@Provider
static func providePump(pump: Thermosiphon) -> Pump {
pump
}This pattern is commonly used to alias a concrete type to a protocol that it conforms to.
All providers must be registered to a module. Modules are just empty enums that have the @Module attribute.
Initializer-based @Providers are included in a module by specifying the provided type via the @Module attribute's provides parameter.
Static @Providers are embedded directly within a module.
@Module(provides: [ElectricHeater.self, Thermosiphon.self, CoffeeMaker.self])
public enum CoffeeModule {
@Provider
static func provideHeater(heater: ElectricHeater) -> Heater {
heater
}
@Provider
static func providePump(pump: Thermosiphon) -> Pump {
pump
}
}The provider functions form a graph of types, linked by their dependencies.
graph LR;
CoffeeMaker-->Heater;
CoffeeMaker-->Pump;
Pump-->Thermosiphon;
Thermosiphon-->Heater;
Heater-->ElectricHeater;
To construct and access the graph, we'll first need to define its roots. That set is defined via a protocol with functions that have no arguments and return the root types.
By applying the @Component attribute the protocol and declaring the modules that can used to provide dependencies, swift-blade then fully generates an implementation of the protocol.
@Component(modules = [CoffeeModule.self])
protocol CoffeeShop {
func maker() -> CoffeeMaker
}The implementation has the same name as the protocol prefixed with "Blade". Now, our CoffeeApp can simply use the generated implementation of CoffeeShop to get a fully dependency-injected CoffeeMaker!
@main
struct CoffeeApp {
static func main() {
let coffeeShop = BladeCoffeeShop()
let coffeeMaker = coffeeShop.maker()
}
}See API Documentation for advanced usage.
Q: How is the dependency graph validated?
Unlike Dagger, a Blade component's dependency graph is validated at runtime immediately upon component initialization. If a dependency does not have a registered provider, a fatalError will occur. This is largely due to the fact that the current macro implementation does not have context APIs that allow a macro to glean semantic information about types found in a delcaration at the time of expansion. If such an APIs were to be added, this validation could potentially occurr at compile time.