I have created a custom propertyWrapper
to inject my dependencies in code so when testing the code, I can pass a mock in place using the WritableKeyPath
link to the object in memory.
This is how I would use it in production code. It is very convenient as I don't need to pass the object inside an initializer.
@Injected(\.child) var child
And this is how I would use it in my unit tests to pass the mock in place of the WritableKeyPath
.
let parentMock = ParentMock()
InjectedDependency[\.parent] = parentMock
The thing is that in some part of the code where I am trying to use it, there seems to be ghost objects that are being created when the Child
class would need to have access to the Parent
class in a cycle. When I am making a look up and play with it in the Playground, I could have noticed that there are two objects created when linked to each others, and only one deallocation for each of them instead of two when setting the variable to nil
.
How can I update my @propertyWrapper
or what could be improved on this solution to make it work as expected? How come two objects are created instead of them making a references to the objects in memory?
So the use in code of this custom dependency injection tool is set below.
I have implemented the classic way with a weak var parent: Parent?
to deallocate the object in memory with no issue to showcase what I was expected.
protocol ParentProtocol {}
class Parent: ParentProtocol {
//var child: Child?
@Injected(\.child) var child
init() { print("🔔 Allocating Parent in memory") }
deinit { print ("♻️ Deallocating Parent from memory") }
}
protocol ChildProtocol {}
class Child: ChildProtocol {
//weak var parent: Parent?
@Injected(\.parent) var parent
init() { print("🔔 Allocating Child in memory") }
deinit { print("♻️ Deallocating Child from memory") }
}
var mary: Parent? = Parent()
var tom: Child? = Child()
mary?.child = tom!
tom?.parent = mary!
// When settings the Parent and Child to nil,
// both are expected to be deallocating.
mary = .none
tom = .none
This is the response in the log when using the custom dependency injection solution.
🔔 Allocating Parent in memory
🔔 Allocating Child in memory
🔔 Allocating Child in memory // Does not appear when using the weak reference.
♻️ Deallocating Child from memory
🔔 Allocating Parent in memory // Does not appear when using the weak reference.
♻️ Deallocating Parent from memory
This is the implementation of my custom PropertyWrapper
to handle the dependency injection following the keys of the Parent
and the Child
for the example of use.
// The key protocol for the @propertyWrapper initialization.
protocol InjectedKeyProtocol {
associatedtype Value
static var currentValue: Self.Value { get set }
}
// The main dependency injection custom tool.
@propertyWrapper
struct Injected<T> {
private let keyPath: WritableKeyPath<InjectedDependency, T>
var wrappedValue: T {
get { InjectedDependency[keyPath] }
set { InjectedDependency[keyPath] = newValue }
}
init(_ keyPath: WritableKeyPath<InjectedDependency, T>) {
self.keyPath = keyPath
}
}
// The custom tool to use in unit tests to implement the mock
// within the associated WritableKeyPath.
struct InjectedDependency {
private static var current = InjectedDependency()
static subscript<K>(key: K.Type) -> K.Value where K: InjectedKeyProtocol {
get { key.currentValue }
set { key.currentValue = newValue }
}
static subscript<T>(_ keyPath: WritableKeyPath<InjectedDependency, T>) -> T {
get { current[keyPath: keyPath] }
set { current[keyPath: keyPath] = newValue }
}
}
// The Parent and Child keys to access the object in memory.
extension InjectedDependency {
var parent: ParentProtocol {
get { Self[ParentKey.self] }
set { Self[ParentKey.self] = newValue }
}
var child: ChildProtocol {
get { Self[ChildKey.self] }
set { Self[ChildKey.self] = newValue }
}
}
// The instantiation of the value linked to the key.
struct ParentKey: InjectedKeyProtocol {
static var currentValue: ParentProtocol = Parent()
}
struct ChildKey: InjectedKeyProtocol {
static var currentValue: ChildProtocol = Child()
}
Many changes, so just compare - in general we need to think about reference counting, ie. who keeps references... and so it works only for reference-types.
Tested with Xcode 13.3 / iOS 15.4
protocol ParentProtocol: AnyObject {}
class Parent: ParentProtocol {
//var child: Child?
@Injected(\.child) var child
init() { print("🔔 Allocating Parent in memory") }
deinit { print ("♻️ Deallocating Parent from memory") }
}
protocol ChildProtocol: AnyObject {}
class Child: ChildProtocol {
//weak var parent: Parent?
@Injected(\.parent) var parent
init() { print("🔔 Allocating Child in memory") }
deinit { print("♻️ Deallocating Child from memory") }
}
protocol InjectedKeyProtocol {
associatedtype Value
static var currentValue: Self.Value? { get set }
}
// The main dependency injection custom tool.
@propertyWrapper
struct Injected<T> {
private let keyPath: WritableKeyPath<InjectedDependency, T?>
var wrappedValue: T? {
get { InjectedDependency[keyPath] }
set { InjectedDependency[keyPath] = newValue }
}
init(_ keyPath: WritableKeyPath<InjectedDependency, T?>) {
self.keyPath = keyPath
}
}
// The custom tool to use in unit tests to implement the mock
// within the associated WritableKeyPath.
struct InjectedDependency {
private static var current = InjectedDependency()
static subscript<K>(key: K.Type) -> K.Value? where K: InjectedKeyProtocol {
get { key.currentValue }
set { key.currentValue = newValue }
}
static subscript<T>(_ keyPath: WritableKeyPath<InjectedDependency, T?>) -> T? {
get { current[keyPath: keyPath] }
set { current[keyPath: keyPath] = newValue }
}
}
// The Parent and Child keys to access the object in memory.
extension InjectedDependency {
var parent: ParentProtocol? {
get { Self[ParentKey.self] }
set { Self[ParentKey.self] = newValue }
}
var child: ChildProtocol? {
get { Self[ChildKey.self] }
set { Self[ChildKey.self] = newValue }
}
}
// The instantiation of the value linked to the key.
struct ParentKey: InjectedKeyProtocol {
static weak var currentValue: ParentProtocol?
}
struct ChildKey: InjectedKeyProtocol {
static weak var currentValue: ChildProtocol?
}
Output for test code: