Wormer is a lightweight and simple dependency injection framework, written in Swift and updated to the 4.2 version of the language.
It is freely inspired by Unity, an open source DI container for the .NET platform (not to be confused with the Unity game engine).
Wormer uses a static declarative approach to link an interface to its implementation. An interface is generally a protocol, but it can also be a base class, whereas an implementation is either a class or a struct adopting that interface, but ideally it can also be any value type adopting the interface protocol.
The dependency injector container is accessed via the Injector
class, which exposes a default
static property. And, if you are wondering, yes, it uses the singleton pattern. The initializer is declared private, to prevent direct instantiation.
Given an interface:
protocol Interface {}
and a class implementing that interface:
final class Implementation : Interface {}
a link is established by invoking the bind
method:
Injector.default.bind(interface: Interface.self, toImplementation: Implementation.self, asSingleton: false, initializer: { Implementation() })
The asSingleton
property specifies, when true
, that a single instance should be created, and always returned - mimicking the singleton pattern. When it's set to false
instead a new instance is created at any invocation of instance()
(see below).
The last parameter initializer
is a closure, which must create and return an instance of the implementation type. Note that this closure is stored internally, so a strong reference is maintained.
Once an interface is bound to an implementation, a new (or cached, in case of a singleton) instance is obtained by invoking the instance(for:)
method:
let instance = Injector.default.instance(for: Interface.self)
An overload of instance(for:)
is available, which takes advantage of type inference for the interface type:
let instance: Interface = Injector.default.instance()
Warning: both implementations internally use force unwrapping to cast the interface to the implementation. That results in a runtime exception if the interface has not been bound to an implementation. A safer methods is available, which doesn't use forced unwrapping, returning an optional instead:
public func safeInstance<P>(for interfaceType: P.Type) -> P?
- an interface can be bound to an implementation only
- an implementation can be bound to more than one interface
Usually it's done when the app starts, so the most appropriate place is probably application(didFinishLaunchingWithOptions:)
. I have the good habit of not overcrowding that method, so I usually create an external struct (or enum) with a static method, doing all initialization.
enum DependencyBuilder {
static func build() {
let injector = Injector.default
/// 1
injector.bind(interface: EventBus.self,
toImplementation: EventBusImplementation.self, asSingleton: true) {
EventBusImplementation()
}
let eventBus: EventBus = injector.instance()
/// 2
injector.bind(interface: NotificationGateway.self,
toImplementation: NotificationGatewayImplementation.self, asSingleton: true) {
NotificationGatewayImplementation(eventBus: eventBus)
}
/// 3
injector.bind(interface: NearableProximityProvider.self,
toImplementation: BrandedNearableProximityProvider.self, asSingleton: false) {
BrandedNearableProximityProvider()
}
}
}
In the above code, three bindings are created:
- The
EventBus
is bound toEventBusImplementation
, singleton enabled - The
NotificationGateway
is bound toNotificationGatewayImplementation
, singleton enabled. Note how the initializer requires anEventBus
instance - The
NearableProximityProvider
is bound toBrandedNearableProximityProvider
, without using the singleton pattern, so a new instance is created at every invocation ofinstance()
As mentioned in the previous paragraph, an instance bound to an interface is obtained using the instance()
method, or its instance(for:)
overload.
There are 2 ways dependencies can be injected:
- in the initializer
- via a property
I tend to favor the former in all cases, except when I can't define a new initializer - a typical example is UIViewController
, whose lifecycle is usually outside of our control, as well as what initializer is used to instantiate it.
Suppose to have a class or struct like this:
struct SomeProvider {
private var eventBus: EventBus
init(eventBus: EventBus) {
self.eventBus = eventBus
}
}
which takes an EventBus
in the initializer. To create an instance:
let eventBus: EventBus = Injector.default.instance()
let provider = SomeProvider(eventBus: eventBus)
When creating an initializer is not an option, then property injection is the only alternative left, at least in Wormer. It can be achieved as follows:
class EventViewController : UIViewController /* NSViewController */ {
private lazy var eventBus: EventBus = Injector.default.instance()
}
EventViewController()
As you can see, I prefer lazy initialization, so that instantiation occurs only if needed.
- iOS: yes
- macOS: not yet
- watchOS: not yet
- tvOS: not yet
pod `wormer`
Sorry, not available yet (help appreciated!!)
Copy the Wormer.swift
file and paste it into your project.
MIT license. Read the LICENSE
file.