Frizlab/GlobalConfModule
Dependency injection or general configuration framework for Swift (any platforms including Linux and Windows)
= Global Conf Module for Swift
François Lamboley fload@me.com
A fully concurrency-ready generic configuration and dependency injection solution for Swift,
loosely inspired by https://www.avanderlee.com/swift/dependency-injection/.
== Basic Usage (Dependency Injection)
Declaring a service:
[source,swift]
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:
[source,swift]
extension ConfKeys {
#declareServiceFactoryKey("myServiceFactory", MyService.self, defaultValue: { DefaultMyService() })
}
Using it:
[source,swift]
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:
[source,swift]
func myAppInit() {
Conf.setRootValue(newService, for: .myService)
}
== Basic Usage (Framework Configuration)
[source,swift]
/* 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]}
}
== Auto-Injected Services
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:
[source,swift]
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:
[source,swift]
struct ServiceClient {
@InjectedConf()
var myService: MyService
...
}
Note services that are protocols cannot be auto-injectable.
== @MainActor or Other Isolated Services
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:
[source,swift]
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.
=== For Global Actors
Basically you will need copy all the *MainActor files and replace the MainActor instances by your own global actor.
=== For Non-Global Actors
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.
== Ordering Service Inits
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:
[source,swift]
struct ServiceClient {
@InjectedConf
var myService: MyService