A compile time dependency injection library for Swift
Sword is a compile time dependency injection library for Swift, inspired by Dagger.
As you declare dependencies and specify how to satisfy them using Swift Macros, Sword automatically generates dependency injection code at compile time. Sword walks through your code and validates dependency graphs, ensuring that every object's dependencies can be satisfied, so there are no runtime errors.
Use the following link to add Sword as a Package Dependency to an Xcode project:
https://github.com/rockname/sword
Important
Do not add the SwordCommand executable to any targets.
Ensure None is selected when asked to choose package products.
Add the following to the package dependencies in your Package.swift:
.package(url: "https://github.com/rockname/sword.git", from: "<version>")Then, include "Sword" as a dependency for your target:
.target(
name: "<target>",
dependencies: [
.product(name: "Sword", package: "sword"),
]
),Sword provides a build tool plugin to generate dependency injection code.
The build tool plugin can be used in both Xcode projects and Swift Package projects.
Note
Requires installing via Xcode Package Dependency.
Add the SwordBuildToolPlugin to the Run Build Tool Plug-ins phase of the Build Phases for the target.
Tip
When using the plugin for the first time, be sure to trust and enable it when prompted. If a macros build warning exists, select it to trust and enable the macros as well.
Note
Requires installing via Swift Package Manager.
Add the plugin to the application root target as follows:
.target(
...
plugins: [.plugin(name: "SwordBuildToolPlugin", package: "Sword")]
),Add the SwordBuildToolPlugin as mentioned above Xcode projects.
Then add a .sword.yml file into your Xcode project's root directory for Sword to read the file and generate your dependency graph considering local Swift Packages.
For example:
local_packages:
- path: PackageA
targets:
- DependencyA
- DependencyB
- path: PackageB
targets:
- DependencyC
- DependencyDConsider an example SwiftUI app with the dependency graph from the following image.
You usually create a Sword dependency graph in your App struct (or root View) because you want an instance of the graph to be in memory as long as the app is running. In this way, the graph is attached to the app lifecycle.
In Sword, @Component is attached to the dependency graph. So you can call it AppComponent. You usually keep an instance of that component in your custom App struct as shown in the following:
// Definition of the App dependency graph
@Component
final class AppComponent {
}
// AppComponent lives in the App struct to share its lifecycle
@main
struct MyApp: App {
let component = AppComponent()
var body: some Scene { ... }
}Instead of creating the dependencies a View requires in the init, you can get a dependency you want from the Component.
struct LoginNavigation: View {
let component: AppComponent
var body: some View {
...
LoginScreen(viewModel: component.loginViewModel)
...
}
}
struct LoginScreen: View {
let viewModel: LoginViewModel
var body: some View { ... }
}Sword needs to know required dependencies to provide the LoginViewModel. You can tell Sword how to initialize LoginViewModel using @Dependency / @Injected like following:
// You want Sword to provide an object of LoginViewModel from the AppComponent graph
@Dependency(registeredTo: AppComponent.self)
final class LoginViewModel {
private let userRepository: UserRepository
@Injected
init(userRepository: UserRepository) {
self.userRepository = userRepository
}
}Let's tell Sword how to provide the rest of the dependencies to build the graph:
@Dependency(registeredTo: AppComponent.self)
final class UserRepository {
private let apiClient: APIClient
@Injected
init(apiClient: APIClient) {
self.apiClient = apiClient
}
}
@Dependency(registeredTo: AppComponent.self)
final struct APIClient {
private let urlSession: URLSession
@Injected
init(urlSession: URLSession) {
self.urlSession = urlSession
}
}When you provide an interface for a dependency, use boundTo parameter on @Dependency.
protocol APIClient { ... }
// You want Sword to provide a DefaultAPIClient implementation for APIClient interface
@Dependency(
registeredTo: AppComponent.self,
boundTo: APIClient.self
)
final struct DefaultAPIClient: APIClient {
...
}For this example, APIClient has a dependency on URLSession. However, the way to create an instance of URLSession is different from what you've been doing until now. It's initializer is defined in the Foundation framework.
Apart from the @Injected, there's another way to tell Sword how to provide a required dependency: the information inside Sword modules. A Sword module is a struct that is attached with @Module. There, you can define dependencies with the @Provider.
// @Module informs Sword that this struct is a Sword Module registered to AppComponent
@Module(registeredTo: AppComponent.self)
struct AppModule {
// @Provider tells Sword how to create the dependency.
@Provider
static func urlSession() -> URLSession {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 30
return URLSession(configuration: configuration)
}
}This is how the Sword graph in the example looks right now:
The entry point to the graph is LoginScreen. Because LoginScreen injects LoginViewModel, Sword builds a graph that knows how to provide an instance of LoginViewModel, and recursively, of its dependencies. Sword knows how to do this because of the @Injected on the dependencies' initializer.
You can use Scope to limit the lifetime of an object to the lifetime of its component. This means that the same instance of a dependency is used every time that type needs to be provided.
To have a unique instance of a UserRepository when you ask for the repository in AppComponent, pass .single to the scopedWith parameter on @Dependency.
@Dependency(
registeredTo: AppComponent.self,
scopedWith: .single
)
final class UserRepository { ... }You can also use the scopedWith parameter in @Provider.
@Module(registeredTo: AppComponent.self)
struct AppModule {
@Provider(scopedWith: .single)
static func urlSession() -> URLSession { ... }
}If your login flow consists of multiple views, you would want to reuse the same instance of LoginViewModel in all views. But you should not use signle scope in AppComponent for the following reasons:
-
The instance of
LoginViewModelwould persist in memory after the login flow has finished. -
You want a different instance of
LoginViewModelfor each login flow. For example, if the user logs out, you want a different instance ofLoginViewModel, rather than the same instance as when the user logged in for the first time.
To scope LoginViewModel to the lifecycle of login flow, you need to create a new component for the login flow.
The new component must be able to access the objects from AppComponent because LoginViewModel depends on UserRepository. The way to tell Sword that you want a new component to use part of another component is with Sword Subcomponent. The new component must be a subcomponent of the component containing shared resources.
In the example, you must define LoginComponent as a subcomponent of AppComponent like following:
// You tell Sword that LoginComponent is a subcomponent of AppComponent
@Subcomponent(of: AppComponent.self)
final class LoginComponent {
}A factory method func makeLoginComponent() -> LoginComponent will be generated in AppComponent.
You call this method when starting the login flow.
@main
struct MyApp: App {
let component = AppComponent()
var body: some Scene {
...
LoginNavigation(component: component.makeLoginComponent())
...
}
}
struct LoginNavigation: View {
let component: LoginComponent
var body: some View {
...
LoginScreen(viewModel: component.loginViewModel)
...
}
}Then, as you set LoginComponent.self to registeredTo and .single to scopedWith on @Dependency of LoginViewModel, the instance of LoginViewModel would be unique in each login flow.
@Dependency(
registeredTo: LoginComponent.self,
scopedWith: .single
)
final class LoginViewModel { ... }Here is how the Sword graph looks with the new subcomponent. The classes with a white dot (UserRepository, URLSession, and LoginViewModel) are the ones that have a unique instance scoped to their respective components.
You can pass some arguments to a component as a dependency.
For example, you can inject environment variables, EnvVars, to AppComponent like following:
struct EnvVars {
let baseURL: URL
}
@Component(arguments: EnvVars.self)
final class AppComponent {
}Then, the @Component macro generates an initializer receiving EnvVars as a parameter.
let component = AppComponent(
envVars: EnvVars(baseURL: URL(string: "https://example.com")!)
)Now you can resolve an EnvVars dependency via AppComponent.
@Dependency(
registeredTo: AppComponent.self,
boundTo: APIClient.self,
scopedWith: .single
)
final class DefaultAPIClient: APIClient {
private let baseURL: URL
@Injected
init(envVars: EnvVars) {
self.baseURL = envVars.baseURL
}
}Assisted injection is a dependency injection (DI) pattern that is used to construct an object where some parameters may be provided by the DI framework and others must be passed in at creation time (a.k.a “assisted”) by the user.
To use Sword’s assisted injection, annotate any assisted parameters with @Assisted, as shown below:
@Dependency(registeredTo: AppComponent.self)
class UserDetailViewModel {
...
@Injected
init(
@Assisted userID: User.ID,
userRepository: UserRepository
) {
self.userID = userID
self.userRepository = userRepository
}
}Then you can pass the assisted parameter when using the dependency as shown below.
struct UserNavigation: View {
let component: AppComponent
var body: some View {
...
UserDetailScreen(viewModel: component.userDetailViewModel(userID: userID))
...
}
}| Feature | Support Status |
|---|---|
| Subcomponent | ✅ Supported |
| Component Arguments | ✅ Supported |
| Single Scope | ✅ Supported |
| Weak Reference Scope | ✅ Supported |
| Assisted Injection | ✅ Supported |
| Missing Dependency Error | ✅ Supported |
| Duplicate Dependency Error | ✅ Supported |
| Cycle Dependency Error | ✅ Supported |
This library is released under the MIT license. See LICENSE for details.