3

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()
}
Roland Lariotte
  • 2,606
  • 1
  • 16
  • 40
  • Playgrounds aren’t suitable for running experiments about memory ownership like this. The playground itself makes strong references to objects for the purpose of displaying them. Try this in a regular, non-playground stand-alone swift file, and run that – Alexander May 04 '22 at 19:31
  • Also, don’t you have a strong reference cycle between parent and child? You still need to use a weak reference, even with your property wrapper. That can be tricky, because `weak` isn’t a type, it’s a property modified, but you can make yourself a box like `struct Weak { let wrapped: T }`, and then define your parent as `@Injected(\.parent) var parent: Weak` – Alexander May 04 '22 at 19:34
  • @Alexander I did try on a stand-alone swift file within a Command Line Tool project and it does exactly the same thing. Two ghosts objects are created. As for your second answer, I did not manage to make your tool working. Could you post a StackOverflow answer with a working response. – Roland Lariotte May 05 '22 at 19:31
  • Not really, I couldn't get these snippets to compile to begin with (`@Injected(\.child) var child` is invalid without a type annotation, and IDK what type it should be), but I would love to help work through it – Alexander May 05 '22 at 19:35

1 Answers1

2

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:

enter image description here

Complete test module in project is here

Asperi
  • 228,894
  • 20
  • 464
  • 690