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
}
}
Is it safe to use closure like this
No.
If you use callTapB
:
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.