If I have a closure in another closure is it enough to use unowned/weak once in the outer closure to avoid retain cycles?
Example:
foo.aClosure({[unowned self] (allowed: Bool) in
if allowed {
self.doStuff()
self.something.anotherClosure({ (s:String) -> (Void) in
self.doSomethingElse(s)
})
}
})
Only declaring weak or unowned self in the capture list of the outer closure is enough to avoid retain cycles if you don't create a strong reference to self within the outer closure (e.g. by doing: guard let strongSelf = self else { return }).
If you do create a strong reference within the closure, you must add a capture list to the inner closure to ensure that it captures your strong reference to self weakly.
Here are some examples:
import Foundation
import PlaygroundSupport
class SomeObject {
typealias OptionalOuterClosure = ((Int) -> Void)?
typealias InnerClosure = () -> Void
var outerClosure: OptionalOuterClosure
func setup() {
// Here are several examples of the outer closure that you can easily switch out below
// All of these outer closures contain inner closures that need references to self
// optionalChecks
// - has a capture list in the outer closure
// - uses the safe navigation operator (?) to ensure that self isn't nil
// this closure does NOT retain self, so you should not see the #2 calls below
let optionalChecks: OptionalOuterClosure = { [weak self] callNumber in
print("outerClosure \(callNumber)")
self?.delayCaller { [weak self] in
print("innerClosure \(callNumber)")
self?.doSomething(callNumber: callNumber)
}
}
// copiedSelfWithInnerCaptureList
// - has a capture list in the outer closure
// - creates a copy of self in the outer closure called strongSelf to ensure that self isn't nil
// - has a capture list in the inner closure
// - uses the safe navigation operator (?) to ensure strongSelf isn't nil
// this closure does NOT retain self, so you should not see the #2 calls below
let copiedSelfWithInnerCaptureList: OptionalOuterClosure = { [weak self] callNumber in
guard let strongSelf = self else { return }
print("outerClosure \(callNumber)")
strongSelf.delayCaller { [weak strongSelf] in
print("innerClosure \(callNumber)")
strongSelf?.doSomething(callNumber: callNumber)
}
}
// copiedSelfWithoutInnerCaptureList
// - has a capture list in the outer closure
// - creates a copy of self in the outer closure called strongSelf to ensure that self isn't nil
// - does NOT have a capture list in the inner closure and does NOT use safe navigation operator
// this closure DOES retain self, so you should see the doSomething #2 call below
let copiedSelfWithoutInnerCaptureList: OptionalOuterClosure = { [weak self] callNumber in
guard let strongSelf = self else { return }
print("outerClosure \(callNumber)")
strongSelf.delayCaller {
print("innerClosure \(callNumber)")
strongSelf.doSomething(callNumber: callNumber)
}
}
// retainingOuterClosure
// - does NOT have any capture lists
// this closure DOES retain self, so you should see the doSomething #2 call below
let retainingOuterClosure: OptionalOuterClosure = { callNumber in
print("outerClosure \(callNumber)")
self.delayCaller {
print("innerClosure \(callNumber)")
self.doSomething(callNumber: callNumber)
}
}
// Modify which outerClosure you would like to test here
outerClosure = copiedSelfWithInnerCaptureList
}
func doSomething(callNumber: Int) {
print("doSomething \(callNumber)")
}
func delayCaller(closure: @escaping InnerClosure) {
delay(seconds: 1, closure: closure)
}
deinit {
print("deinit")
}
}
// Handy delay method copied from: http://alisoftware.github.io/swift/closures/2016/07/25/closure-capture-1/
func delay(seconds: Int, closure: @escaping () -> Void) {
let time = DispatchTime.now() + .seconds(seconds)
DispatchQueue.main.asyncAfter(deadline: time) {
print("🕑")
closure()
}
}
var someObject: SomeObject? = SomeObject()
someObject?.setup()
// Keep a reference to the outer closure so we can later test if it retained someObject
let copiedOuterClosure = someObject!.outerClosure!
// Call the outer closure once just to make sure it works
copiedOuterClosure(1)
// Wait a second before we destroy someObject to give the first call a chance to work
delay(seconds: 1) {
// Run the outerClosure again to check if we retained someObject
copiedOuterClosure(2)
// Get rid of our reference to someObject before the inner closure runs
print("de-referencing someObject")
someObject = nil
}
// Keep the main run loop going so our async task can complete (need this due to how playgrounds work)
PlaygroundPage.current.needsIndefiniteExecution = true
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With