It's a Swift Dependency Injection Framework that is called “Macaroni”. Cut Spaghetti Code into pieces! :–)
When I start my projects, I need some kind of DI. It's obvious that property wrappers can be used for DI framework. Here it is.
Macaroni uses a hack from this article https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/
to be able to access self
of the enclosing object. There is a limitation because of that: @Injected
can be used only in reference types,
because properties are being lazy initialized when accessed first time, thus changing the container
(which is problematic to do with value types).
Please look at UPDATE.md to find out about migrations.
Please use Swift Package Manager.
Repository address: [email protected]:bealex/Macaroni.git
or https://github.com/bealex/Macaroni.git
.
Name of the package is Macaroni
.
Current version is v4.x
// Create the container.
let container = Container()
// Set it as a singleton for the simplest service-locator style resolution.
Container.lookupPolicy = .singleton(container)
// Add service implementations into the container.
let myService = MyServiceImplementation()
container.register { () -> MyService in myService }
// Use it in code.
let myService: MyService = container.resolve()
// Or use it with property wrapper.
class MyClass {
@Injected
var service: MyService
}
First let's import Macaroni and prepare our protocol and implementation that we want to inject.
import Macaroni
protocol MyService {}
class MyServiceImplementation: MyService {}
Macaroni should know where container is placed, to get objects for injection. You can think of container as a box
that holds all the objects. The knowledge of where container is placed is defined by Container.lookupPolicy
.
Let's use simple service locator policy, that uses
a singleton
object to hold all the objects that can be injected.
let container = Container()
Container.lookupPolicy = .singleton(container)
To register something inside a container, we register a resolver there. Resolver is a closure that returns instance of a specific type. It can return same instance all the time, can create it each time it is accessed. You choose. For now let's register the resolver, that returns same instance every time it is used.
let myService = MyServiceImplementation()
container.register { myService }
And then we can inject this value like this:
class MyClass {
@Injected
var myService: MyServiceImplementation
}
Usually we need to be able to use it with the protocol like this: var myService: MyService
, not with the implementation
type (var myService: MyServiceImplementation
). For that we need to tell Container
, that if it is being asked of MyService
,
it should inject this specific object. It can be done using one of two options:
// 1.
// Now myService is of type `MyService` and registration will be
// typed as `() -> MyService` instead of `() -> MyServiceImplementation`
let myService: MyService /* <- Magic happens here */ = MyServiceImplementation()
container.register { myService }
// 2.
// or like this (I prefer this option):
let myService = MyServiceImplementation()
container.register { () -> MyService /* <- Magic happens here */ in myService }
Please note that injection is happening lazily, not during
MyController
initialization but whenmyService
is first accessed.
In the code above, implementation is being created right away. If you want to lazily create objects that should be injected, you can use a wrapper like this:
class LazilyInitialized<Type> {
lazy var value: Type = { resolver() }()
private let resolver: () -> Type
init(resolver: @escaping () -> Type) {
self.resolver = resolver
}
}
let willBeInstantiatedOnFirstAccess = LazilyInitialized { MyServiceImplementation() }
container.register { () -> MyService in willBeInstantiatedOnFirstAccess.value }
// 1.
// Lazy injection from the container that is captured on initialization, determined by `Container.policy`:
@Injected
var property: Type
// 2.
// Lazy injection from the container that is captured on initialization (you specify it):
@Injected(.capturingContainerOnInit(from: container))
var property: Type
// 3.
// Lazy capturing of the container and resolving:
@Injected(.lazily)
var property: Type
// 4.
// Eager resolving, during the initialization, from the container from `Container.policy`:
@Injected(.resolvingOnInit())
var property: Type
// 5.
// Eager resolving, during the initialization, from the specified container:
@Injected(.resolvingOnInit(from: container))
var property: Type
Please note that parametrized injection works only when object is being resolved lazily. Eager injection can only resolve objects by type (and alternative if it is provided).
Also lazy injection can't be used in
structs
, because it needs to modify object after the resolve.
// - create alternative identifier. Strings must be different for different types.
extension RegistrationAlternative {
static let another: RegistrationAlternative = "another"
}
// - registration
container.register(alternative: .another) { () -> MyService in anotherInstance }
// - injection
@Injected(alternative: .another)
var myServiceAlternative: MyService
Starting from Swift 5.5 we can use property wrappers for function parameters too. Here is the function declaration:
func foo(@Injected service: MyService) { /* Use service here */ }
And its call using default instance:
foo($service: container.resolved())
Or alternative instance
foo($service: container.resolved(alternative: .another))
If you need to use object that contains the injected property, you can get from inside registration closure like this:
container.register { enclosing -> String in String(describing: enclosing) }
This resolver will be available for lazy injections only.
When using property wrappers, you can't use weak
(or lazy
or unowned
). If you need that, you can use @InjecteadWeakly
.
@InjectedWeakly
var myService: MyService?
There are three policies of container selection for properties of specific enclosing object:
- service locator style. It is called
singleton
, and can be set up like this:Container.lookupPolicy = .singleton(myContainer)
. - enclosing object based. This policy implies, that every enclosing type implements
Containerable
protocol that definesContainer
for the object. You can set it up with.enclosingType(default:)
. - custom. If you want to control container finding yourself and no other option suits you, you can implement
ContainerLookupPolicy
yourself.
If your application uses several modules and each module needs its own Container
, you can use this option:
Write this somewhere in the common module:
protocol ModuleDI: Containerable {}
Container.lookupPolicy = EnclosingTypeContainer()
And this in each module:
private var moduleContainer: Container!
extension ModuleDI {
var container: Container! { moduleContainer } // now each module does have its own container
}
class MyClass: ModuleDI {
@Inject var service: MyService // will be injected from the `moduleContainer`
}
Macaroni does not do anything about multithreading. Please handle it yourself if needed.
By default, Macaroni will print simple events (container creation, resolver registering, injections) to the console. If you don't need that
(or need to alter logs in some way), please set Macaroni.Logger
to your implementation of MacaroniLogger
:
class MyMacaroniLogger: MacaroniLogger {
func log(/* Parameters */) { /* Logging code */ }
func die() -> Never { /* Log and crash */ }
}
Macaroni.logger = MyMacaroniLogger()
Use this code to disable logging completely:
Macaroni.logger = DisabledMacaroniLogger()
License: MIT, https://github.com/bealex/Macaroni/blob/main/LICENSE