An implementation of dependency injection pattern. kvEnvironment provides:
- simple API with strong typing;
- hierarchy of scopes with cascade resolution of properties;
- thread-safety;
- flexible ways to override scopes;
- retain-cycles-free architecture;
- lazy initialization of properties.
There are no explicit restrictions for any platform. So it's assumed that kvEnvironment can be compiled on any platform Swift is available on.
.package(url: "https://github.com/keyvariable/kvEnvironment.git", from: "0.5.2")
.product(name: "kvEnvironment", package: "kvEnvironment")
import kvEnvironment
extension KvEnvironmentScope {
#kvEnvironment { var someProperty: SomeType }
}
@KvEnvironment(\.someProperty) private var someProperty
Below is an example where C
depends on A
and B
:
struct A { let a: Int }
struct B { let b: String }
extension KvEnvironmentScope {
#kvEnvironment {
var a: A?
var b: B = .init(b: "default")
}
}
struct C {
@KvEnvironment(\.a) private var a
@KvEnvironment(\.b) private var b
}
Environment property a
is declared as optional.
#kvEnvironment
macro provides implicit default value nil
for optional types.
Environment property b
is declared as an opaque type and has a default value.
So C
can be instantiated in any scope at any moment due to the defaults are provided.
If an environment property is defined with no default value, then it have to be initialized explicitly before it's getter is invoked.
There are global (KvEnvironmentScope.global
) and task-local (KvEnvironmentScope.current
) scopes.
It's possible to create standalone or overriding scopes:
// By default new scopes override global scope.
let aScope = KvEnvironmentScope {
$0.a = A(a: 1)
}
// Parent scope can be explicitly provided.
// So in `abScope` both `a` and `b` properties are overridden.
let abScope = KvEnvironmentScope(parent: aScope) {
$0.b = B(b: "custom")
}
Below is an example of a way to temporary override global scope:
let c = C()
// Here dependencies are resolved in the global scope.
print(c)
// In block below current scope is changed to `abScope`.
abScope {
// Here dependencies are resolved in `abScope`.
print(c)
}
There are several ways to provide explicit scope to dependency references:
- in attribute declaration
@Environment(\.a, scope: someScope) private var a
; - when a scope is instantiated, all it's direct properties refer to the scope;
- via projected value
$a.scope = someScope
; - it possible to change all scope references of arbitrary instances via
replace(in:options:)
method ofKvEnvironmentScope
.
struct E {
let id: String
@KvEnvironment(\.f) var f
}
struct F {
let id: String
@KvEnvironment(\.e) var e
}
extension KvEnvironmentScope {
#kvEnvironment {
var e: E
var f: F
}
}
struct G: CustomStringConvertible {
@KvEnvironment(\.e) private var e
@KvEnvironment(\.f) private var f
}
// Populating global scope with required values.
KvEnvironmentScope.global {
$0.e = E(id: "e1")
$0.f = F(id: "f1")
}
let g = G()
// e: "e1", f: "f1", e.f: "f1", f.e: "e1".
print(g)
- Svyatoslav Popov (@sdpopov-keyvariable, [email protected]).