A dependency injection library for Swift that statically generates resolvers at compile-time
Toledo
Toledo is a dependency injection library for Swift that statically generates resolvers at compile-time.
Features
- once it compiles, it works
- async and throwing dependencies
- concurrency support
- multiple containers (no singleton)
- makes no assumption about your code
- conformance can be provided in extensions
- works great with SwiftUI for view model DI
- simple installation process via SPM
Installation
Using Swift Package Manager:
dependencies: [
.package(
name: "Toledo",
url: "https://github.com/valentinradu/Toledo.git",
from: "0.1.0"
)
],
targets: [
.target(
name: "MyTarget",
dependencies: ["Toledo"],
plugins: [
.plugin(name: "ToledoPlugin", package: "Toledo")
]
)
]
Notice the plugin. It should be applied to all targets that use the library.
Usage
Toledo has 3 types of dependencies: regular, throwing and async throwing. Each has its own protocol that needs to be implemented for a type to be available in the dependency container. For example the conformance for a final class IdentityModel
to AsyncThrowingDependency
would look like this:
extension IdentityModel: AsyncThrowingDependency {
public convenience init(with container: SharedContainer) async throws {
await self.init(profile: try await container.profile(),
settings: container.settings())
}
}
At compile time, Toledo will look for types conforming to Dependency
, ThrowingDependency
or AsyncThrowingDependency
and will store shared instances of each on SharedContainer
.
This means that the IdentityModel
above will be available everywhere as try await container.identityModel()
as long as you have a reference to the container. Notice how an async throwing dependency requires try await
to resolve. If IdentityModel
would have been a regular dependency, container.identityModel()
would have been enough.
Shared instances vs new instances
Calling container.identityModel()
always returns the same instance, even in multi-threaded contexts. If you wish to create a new instance within a given container, use the init(with:)
directly:
let newInstance = IdentityModel(with: container)
Providing overrides
If you wish to provide alternative values for some of your dependencies (i.e. for testing) you can do so by setting the SharedContainer
provider:
var container = SharedContainer()
container.replaceProvider(ProfileDependencyProviderKey.self) { _ in
MockedProfile()
}
let mockedInstance = try await container.identityModel()
Providing a resolving protocol
If you need your dependency to resolve to a protocol you can use ResolvedTo
.
extension IdentityModel: IdentityModelProtocol { ... }
extension IdentityModel: AsyncThrowingDependency {
public typealias ResolvedTo = IdentityModelProtocol
public convenience init(with container: SharedContainer) async throws {
await self.init(profile: try await container.profile(),
settings: container.settings())
}
}
Now try await container.identityModel()
will return IdentityModelProtocol
instead of IdentityModel
.
Important note: Make sure the dependency implements the protocol.
Concurrency
Toledo uses Swift’s concurrency model for AsyncThrowingDependency
and a simple semaphore for the other dependencies to guarantee that shared instances are never instantiated more than once per container.