A swift package to support dependency resolution with property wrapper support for ease of use.
NOTE: Whilst convenient for prototyping using this with non throw away code could be a maintainence issue if used without restrictions; since this obscures property ownership and at worst could be encouraging the action at a distance anti-pattern.
This is how you would do something similar to Swinject basic usage
let resolver = DependencyResolver()
resolver.register { Cat(name: "Mimi") as Animal }
resolver.register { PetOwner() as Person }
let petOwner: Person = resolver.resolve()
petOwner.play()
// print: I'm playing with Mimi.
Where the types are defined as follows.
protocol Animal {
var name: String? { get }
}
class Cat: Animal {
let name: String?
init(name: String?) {
self.name = name
}
}
protocol Person {
func play()
}
class PetOwner: Person {
@Resolve var pet: Animal
func play() {
let name = pet.name ?? "someone"
print("I'm playing with \(name).")
}
}
Resolving registered dependencies is simple just add the @Resolve
property wrapper in front of your property.
@Resolve var pet: Animal
This will find the first DependencyResolver
that registered this type to resolve the value.
If using a single DependencyResolver
per type variant is not appropriate for your use case you can include this as part of your property declaration.
@Resolve(resolver: someContext) var pet: Animal
Before the above will work there must be a defined way to resolve the object that will be returned. This is done by registering a closure that returns the type to be resolved.
Note the casting to Animal
, this is allows registration of a new Cat
instance any time we resolve the Animal
type.
let resolver = DependencyResolver()
resolver.register { Cat(name: "Mimi") as Animal }
There can be only a single registration for a given type variant this allows the default registrations to be ignored which might be useful for testing purposes. Earlier registration of mock/stub objects will take precedence allowing you to provide alternate implementation for testing purposes.
If an alternate registration is truly required the old registration can be removed and a new one registered.
resolver.removeResolver(for: Person.self)
There can be multiple variants registered for a single type.
resolver.register(variant: "long_date") { () -> DateFormatter in
let formatter = DateFormatter()
formatter.dateFormat = "MMM yyyy"
return formatter
}
resolver.register(variant: "short_date") { () -> DateFormatter in
let formatter = DateFormatter()
formatter.dateFormat = "dd/MM/yyyy"
return formatter
}
// This will resolve expected date formatter
@Resolve(variant:"long_date") var formatter: DateFormatter
Resolve does not require explicit management of the lifetime of any resolved objects. The above date example would need to be modified in order to prevent a new date formatter being created everytime it was resolved.
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMM yyyy"
return formatter
}()
resolver.register(variant: "long_date") { dateFormatter }
Objects lifetime can be specified explictly using the convenience functions persistent
, transient
, ephemeral
.
// persistent life time will always resolve the same object
resolver.persistent { Example() }
// transient life time will resolve the same object provided there is a strong reference to it elsewhere
resolver.transient { Example2() }
// ephemeral life time will always resolve a new object
resolver.ephemeral { Example3() }
Assigning to the formatter property would currently be a no-op but backing storage can be updated by implementing a storer closure.
var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMM yyyy"
return formatter
}()
resolver.register(variant: "long_date", resolver: { dateFormatter }, storer: { f in dateFormatter = f })
The above registration will allow the following to property to be used as a setter as well.
// This will resolve expected date formatter
@Resolve(variant:"long_date") var formatter: DateFormatter
The property can be set directly or via calling the store function on the DependencyResolver
.
self.formatter = someOtherFormatter
// OR
resolver.store(object: someOtherFormatter, variant: "long_date")
Type variants registered with the persistent
or transient
functions may have thier stored values replaced.
let petOwner = resolver.register { PetOwner() }
let mimi = resolver.transient(variant: "Mimi") { Cat(name: "Mimi") as Animal }
petOwner.play()
// print: I'm playing with Mimi.
petOwner.pet = Cat(name: "Franky")
petOwner.play()
// print: I'm playing with Franky.
When registering a type conforming to DependencyRegister
protocol the registerDependencies
function will be called giving you an opportunity to register any further dependencies.
This allows the distribution of dependency registration through out the application in hierarchical manner, as one register may register another whilst in turn registering its own dependencies.
final class ExampleRegister: DependencyRegister {
func registerDependencies(resolver: Resolver) {
resolver.register(variant: "Mimi") { Cat(name: "Mimi") as Animal }
resolver.register { PetOwner() as Person }
}
}
let resolver = DependencyResolver()
resolver.register { ExampleObject() }
let petOwner: Person = resolver.resolve()
petOwner.play()
// print: I'm playing with Mimi.