swiftdependency-injectionproperty-wrapper

How to avoid a retain cycle on my custom dependency injection tool?


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()
}

Solution

  • 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