A fully concurrency-ready generic configuration and dependency injection solution for Swift, loosely inspired by https://www.avanderlee.com/swift/dependency-injection/.
Declaring a service:
public protocol MyService : Sendable { ... }
public final class DefaultMyService : MyService { ... }
extension ConfKeys {
#declareServiceKey("myService", MyService.self, defaultValue: DefaultMyService())
}
/* You can have as many injected instances of your service as you want by declaring more keys for it. */
Declaring a service which is instantiated via a factory:
extension ConfKeys {
#declareServiceFactoryKey("myServiceFactory", MyService.self, defaultValue: { DefaultMyService() })
}
Using it:
struct ServiceClient {
@InjectedConf(\.myService) var myService: MyService
@InjectedConf(\.myServiceFactory) var myServiceViaFactory: MyService
func myFunction() {
/* The myService variable will always be up-to-date. */
myService.doAmazingStuff(...)
/* Using the default definition set above for myServiceFactory, myServiceViaFactory will always be a new instance. */
myServiceViaFactory.doAmazingStuff(...)
}
}
Changing the injected instance:
func myAppInit() {
Conf.setRootValue(newService, for: \.myService)
}
/* Declare the configuration key. */
extension ConfKeys {
#declareConfKey("myConf", Int.self, defaultValue: 42)
}
/* Use it in your code. */
if Conf[\.myConf] == 42 {...}
/* Optionally you can define an internal convenience on Conf for easier access. */
internal extension Conf {
static var myConf: Int {Conf[\.myConf]}
}
An auto-injected service is a service that can be @InjectedConf
w/o specifying a keypath in the property wrapper init.
This is useful in case there is a “main” instance of your service and specifying a key-path each time the service variable is defined would be redundant.
For these services you can use the AutoInjectable
protocol:
public final class MyService { ... }
extension ConfKeys {
#declareServiceKey("myService"/* or nil if you don’t need the ConfKeys keypath */, MyService.self, "MyServiceKey", defaultValue: DefaultMyService())
}
extension MyService : AutoInjectable {
public typealias AutoInjectionKey = ConfKeys.MyServiceKey
}
And getting the service instance can now be done like so:
struct ServiceClient {
@InjectedConf()
var myService: MyService
...
}
Note services that are protocols cannot be auto-injectable.
For @MainActor
services you must use the ConfKeyMainActor
and AutoInjectableMainActor
protocols instead of their non-MainActor
equivalents.
The macro have an argument to pass the global actor to use to declare the conf or service:
extension ConfKeys {
#declareConfKey("myConf", Int.self, on: MainActor.self, defaultValue: 42)
}
For services isolated on other global (or non-global) actors you’ll have to do a little bit more boilerplate.
Basically you will need copy all the *MainActor
files and replace the MainActor
instances by your own global actor.
This is probably a very advance use-case. If you need that, ask yourself why and see if you really do.
Like for global actors you will need to copy and adapt the *MainActor
files.
I have not tried it, but most likely you will have to remove the global actor annotations and add the isolated
parameter you need in the different methods in the files.
In the very rare case where you need control on when the service is initialized during the init of your client, you can use the following pattern:
struct ServiceClient {
@InjectedConf
var myService: MyService
init() {
/* Do whatever... */
_myService = .init()
/* Do something else... */
}
}