Search code examples
swiftxcode

Is it safe to call an optional closure without [weak self]?


I have two classes, class A and class B. In class A I have declared an optional closure and from class B I am calling that closure. In class B, you can see in callTapA function I am calling the closure with [weak self] to safely release the reference, but in callTapB function I am calling the optional closure and assigning with a function. Both are correct but how is the [weak self] is managed in callTapB ? Is it safe to use closure like this?

class A {
      var onTapA: ((_ name: String) -> Void)?

      init(onTapA: ((_ name: String) -> Void)? = nil) {
          self.onTapA = onTapA
      }
}

class B {
     var newName:String = ""
     let a:A

     init(a: A) {
         self.a = a
    
         callTapA()
         callTapB()
     }

    
    func callTapA() {
        a.onTapA = { [weak self] name in
            guard let self = self else { return }
            newName = name
        }
    }
    
    func callTapB() {
        a.onTapA = onTapB
    }
    
    func onTapB(name: String) {
        newName = name
    }

}

Solution

  • Is it safe to use closure like this

    No.

    If you use callTapB:

    • The B instance was initialized with the A instance as a parameter, and has stored it in a strong reference (a property).
    • Then callTapB() comes along and stores a B instance function in a strong reference in A (a property).

    Therefore, the A and B instances are retaining each other; that is a retain cycle and a memory leak.

    If you use just callTapA, on the other hand, that's not the case; both are instances released in good order.

    It is easy to confim this by modifying your code to include a test harness and deinit implementations. I did it like this:

    import UIKit
    
    class ViewController: UIViewController {
    
        class A {
            var onTapA: ((_ name: String) -> Void)?
    
            init(onTapA: ((_ name: String) -> Void)? = nil) {
                self.onTapA = onTapA
            }
    
            deinit {
                print("farewell from A")
            }
        }
    
        class B {
            var newName:String = ""
            let a:A
    
            init(a: A) {
                self.a = a
    
                // callTapA()
                // callTapB()
            }
    
    
            func callTapA() {
                a.onTapA = { [weak self] name in
                    guard let self = self else { return }
                    newName = name
                }
            }
    
            func callTapB() {
                a.onTapA = onTapB
            }
    
            func onTapB(name: String) {
                newName = name
            }
    
            deinit {
                print("farewell from B")
            }
        }
    
    
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                let theB = B(a: A())
                DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                    print(theB)
                }
            }
        }
    }
    

    Run the code twice, once with just callTapA() uncommented, and the other with just callTapB() uncommented. You will see that, the first way, both A and B go out of existence in good order, but the second way, neither does.

    Note, too, please, that the fact that onTapA is an Optional is irrelevant. It allows you to set onTapA after the initialization but it has nothing whatever to do with how memory is managed.