Strange retain cycle

The following playground example demonstrates two blocks of code, one of which creates a retain cycle and the other that does not. The only difference between the two code blocks is the placement of the guard clause. Can someone help explain how the top example creates a retain cycle since tempClosure is not executed within the closure? Or could this be a swift bug?

import Foundation

class Example {
    var closure: () -> () = { }
    var innerClosure: () -> () = { }

    func hasRetainCycle() {
        var tempClosure: () -> () = { }
        closure = { [weak self] in
            guard let `self` = self else { return }
            tempClosure = {
                self.innerClosure = {
                    _ = self
                }
            }
        }
        closure()
    }
    
    func doesNotHaveRetainCycle() {
        var tempClosure: () -> () = { }
        closure = { [weak self] in
            tempClosure = {
                guard let `self` = self else { return }
                self.innerClosure = {
                    _ = self
                }
            }
        }
        closure()
    }
}

func showCycles() {
    weak var hasRetainCycle: Example?
    weak var doesNotHaveRetainCycle: Example?

    autoreleasepool {
        let example1 = Example()
        example1.hasRetainCycle()
        hasRetainCycle = example1
        
        let example2 = Example()
        example2.doesNotHaveRetainCycle()
        doesNotHaveRetainCycle = example2
    }

    assert(hasRetainCycle != nil)
    assert(doesNotHaveRetainCycle == nil)
}

showCycles()

I don’t think this is a bug. As soon as you unwrap self you get a strong retain on it. So when that self is captured by the second closure, you get a strong capture immediately.

At what point does self become unwrapped, though? I think that’s what I’m confused about. Observe, if you call tempClosure() in the 2nd function after instantiating it, then that creates the retain cycle. The first function, doesn’t call it yet still has a cycle.

tempClosure is not actually temporary. It is captured and kept alive by closure, which is a stored property.

In the first example, tempClosure captures a strong reference to self, so there is a retain cycle (selfclosuretempClosureself).

In the second example, tempClosure captures a weak reference to self, so there is no retain cycle (selfclosuretempClosureself).

1 Like

Thank you, that adds clarity. I was able to reduce function 1 to the below, which I found a little easier to understand. The additional boilerplate made it a little fuzzy.

func hasRetainCycle() {
    closure = {
        self.innerClosure = {
            _ = self
        }
    }
}