swiftclosuresstrong-references

Property Initialization with closures


I was looking into ARC and strong reference cycles and ran into this code of mine:

class TestClass: UIView {
  let button: UIButton = {
    let view = UIButton()
    view.frame = CGRect(x: 50, y: 50, width: 200, height: 200)
    view.backgroundColor = .blue
    view.translatesAutoresizingMaskIntoConstraints = false
    view.setTitle("Button", for: .normal)
    view.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
    return view
  }()

  @objc private func buttonClicked() {
    print("Clicked")
  }

  override init(frame: CGRect) {
    super.init(frame: frame)
    print("Object of TestClass initialized")
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  deinit {
    print("Object of TestClass deinitialized")
  }
}

reference to self in the addTarget method inside the closure doesn't seem to create a strong reference cycle.

Can someone explain why?

Also, I noticed that if I remove inheritance from UIView the compiler starts complaining: Use of unresolved identifier 'self'.

Can someone explain this as well, why does it happen in this case and doesn't in the first one?


Solution

  • This is not a retain cycle because self is not what you think it is :)

    Properties with initial value are "executed" even before any initializer runs, and for those properties self points to a higher order function of this type:

    (TestClass) -> () -> TestClass
    

    So you don't really access the instance, but rather you access a static-like method that does the initialization of all properties that have a default value. This is why you don't have a retain cycle.

    addTarget accepts an Any? value for it's first argument, so this violates no type rules so the compiler doesn't complain that you don't pass a NSObject instance there.

    Checking the later behaviour - e.g. what happens if the button is added to the UI hierarchy and is tapped, reveals something interesting: the runtime sees that you passed a non-object as a target and sets null values for target and action:

    enter image description here