SparkDI is a dependency injection framework in Swift, designed for speed and performance, inspired by industry best practices. It aims to provide a simple and efficient dependency injection solution for Swift projects, with support for scopes and flexible dependency resolution.
- Constructor, Property, and Method Dependency Injection
- Scope Management: Singleton and Transient
- Property Wrapper Support: Use @Dependency for cleaner, safer dependency injection
- Actor-Based Thread Safety: Modern Swift Concurrency for managing concurrent access
- Modular Dependency Registration: Organize and manage dependencies with an Assembler and modules
- Support for Dependencies with Multiple Arguments
- Circular Dependency Detection: Automatically detects and prevents circular dependencies
- Type Registry: Enhanced type safety and dependency tracking
- Improved Error Handling: Clear error messages for common dependency injection issues
Add SparkDI via Swift Package Manager:
dependencies: [
.package(url: "https://github.com/sassiwalid/SparkDI.git", from: "0.1.0")
]
The DependencyContainer is the core of SparkDI, where you register and resolve dependencies
let container = DependencyContainer()
To register a dependency, use the register method. You can specify the type, a factory closure to create the instance, and an optional scope (.singleton or .transient).
// Registering a singleton instance
try await container.register(type: String.self, instance: { "Singleton Instance" }, scope: .singleton)
// Registering a transient instance
try await container.register(type: Int.self, instance: { 42 }, scope: .transient)
• .singleton scope creates the instance once and reuses it for every resolution.
• .transient scope creates a new instance each time the dependency is resolved.
To get an instance of a dependency, use the resolve method:
// Resolving the singleton instance
let singletonString: String? = try await container.resolve(type: String.self)
print(singletonString) // Output: Singleton Instance
// Resolving the transient instance
let transientInt: Int? = try await container.resolve(type: Int.self)
print(transientInt) // Output: 42
SparkDI supports resolving dependencies that require multiple arguments by allowing resolve to accept a variable number of arguments.
You can register a dependency that requires multiple arguments by using a factory closure that takes an array of Any types. This array will be used to pass in the required arguments when resolving the dependency.
try await container.register(
type: String.self,
factory: { args in
guard let name = args[0] as? String, let age = args[1] as? Int else {
return "Invalid arguments"
}
return "\(name) is \(age) years old"
},
scope: .transient
)
When resolving a dependency that requires arguments, pass the arguments in the resolve method as a variadic list.
let instance: String? = try await container.resolve(
type: String.self,
arguments: ["Mohamed", 40]
)
This functionality allows for flexible dependency resolution, supporting dependencies that require parameters at runtime.
For larger projects, you can organize dependencies into modules using the Assembler. Each module registers a group of dependencies, and the assembler applies these modules to a single DependencyContainer.
Define each module as a struct conforming to the Module protocol, which provides a method to register dependencies in the container.
struct NetworkModule: Module {
func registerDependencies(in container: DependencyContainer) {
try await container.register(
type: APIService.self,
instance: { APIService() },
scope: .singleton
)
try await container.register(
type: NetworkManager.self,
instance: { NetworkManager() },
scope: .transient
)
}
}
struct UserModule: Module {
func registerDependencies(in container: DependencyContainer) {
try await container.register(
type: UserService.self,
instance: { UserService() },
scope: .singleton
)
try await container.register(
type: UserSession.self,
instance: { UserSession() },
scope: .transient
)
}
}
Create an Assembler and apply the modules to register dependencies.
let assembler = Assembler()
assembler.apply(modules: [NetworkModule(), UserModule()])
// Resolving dependencies
let apiService: APIService? = await assembler.resolve(type: APIService.self)
let userService: UserService? = await assembler.resolve(type: UserService.self)
The @Dependency property wrapper simplifies dependency injection in classes, structs, or views. It integrates seamlessly with the assembler.
Step 1: Define a Class with Dependencies
class ViewController {
@Dependency(assembler) var service: SomeService
func load() async {
try await $service.resolve() // Asynchronously resolve the dependency
service.performAction()
}
}
Step 2: Configure Dependencies
let container = DependencyContainer()
let assembler = Assembler(container: container)
Task {
try await container.register(SomeService.self) { SomeService() }
let viewController = ViewController()
await viewController.load() // Output: Service is performing an action!
}
Advantages of the @Dependency Property Wrapper and Actor-Based Thread Safety
• Simplified Code: The property wrapper reduces boilerplate for dependency injection.
• Concurrent Safety: Actors protect shared state, ensuring stability during heavy multithreading.
• Asynchronous Compatibility: Both @Dependency and actors work natively with Swift’s async/await.
SparkDI uses actors instead of NSLock or other manual synchronization mechanisms to ensure thread safety. Why Use Actors?
• Automatic Synchronization: Actors serialize access to their state, ensuring thread safety without requiring manual locks.
• Modern Concurrency: Aligns with Swift’s concurrency model (async/await) for safer, more scalable multithreading.
• Performance: Actors reduce the risk of deadlocks and race conditions, improving stability under heavy concurrent access.
SparkDI automatically detects circular dependencies during resolution:
// This will throw a circular dependency error
class ServiceA {
@Dependency var serviceB: ServiceB
}
class ServiceB {
@Dependency var serviceA: ServiceA
}
The framework uses a depth-first search algorithm (DFS) to detect cycles in the dependency graph, preventing infinite loops and initialization deadlocks.
• Additional scope options for more flexible dependency management.
• Optimizations for larger dependency graphs.
MIT License