Do capture lists of inner closures need to redeclare `self` as `weak` or `unowned`?

The [weak self] in anotherFunctionWithTrailingClosure is not needed.

You can empirically test this:

class Experiment {
    func someFunctionWithTrailingClosure(closure: @escaping () -> Void) {
        print("starting", #function)
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing", #function)
        }
    }

    func anotherFunctionWithTrailingClosure(closure: @escaping () -> Void) {
        print("starting", #function)
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing", #function)
        }
    }

    func doSomething() {
        print(#function)
    }

    func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            self?.anotherFunctionWithTrailingClosure { // [weak self] in
                self?.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

And then:

func performExperiment() {
    DispatchQueue.global().async {
        let obj = Experiment()

        obj.testCompletionHandlers()

        // sleep long enough for `anotherFunctionWithTrailingClosure` to start, but not yet call its completion handler

        Thread.sleep(forTimeInterval: 1.5)
    }
}

If you do this, you will see that doSomething is never called and that deinit is called before anotherFunctionWithTrailingClosure calls its closure.

That having been said, I might still be inclined to use the [weak self] syntax on anotherFunctionWithTrailingClosure to make my intent explicit.


TL;DR

Although using [weak self] once in the outer block is fine (EX1), if you change this reference to strong (e.g. guard let self = self), you'll need a [weak self] in the inner block as well (EX3).

Also using [weak self] only once on the inner block is typically an error (EX2_B). Unfortunately, this is a common mistake to make when refactoring code, and can be hard-to-spot when it is made.


A good rule of thumb is to always use weak if the object is strong immediately outside of the closure.

Examples that don't retain self (i.e. typically these are the "good" scenarios):

// EX1
fn { [weak self] in
  self?.foo() 
}
// EX2
fn { [weak self] in 
  fn2 {
    self?.foo()
  }
}
// self is weak inside fn, thus adding an extra `[weak self]` inside fn2 is unnecessary
// EX3
fn { [weak self] in 
  guard let self = self else { return }
  fn2 { [weak self] in
    self?.foo()
  }
}

Examples that DO retain self (i.e. typically the "bad" scenarios):

// EX1_B
fn {
  self.foo()
}
// fn retains self
// EX2_B
fn {
  fn2 { [weak self] in
    self.foo()
  }
}
// fn retains self (this is a common, hard-to-spot mistake)
// EX3_B
fn { [weak self] in 
  guard let self = self else { return }
  fn2 {
    self.foo()
  }
}
// fn2 retains self

As Hamish alludes to, there are two main reasons weak is useful:

  1. To prevent retain cycles.
  2. To prevent objects living longer than they should be.

More on #2 (preventing long lived objects)

In Rob's example, the function is not retaining the closure (beyond dispatch_async which is all but guaranteed to fire the closure at some point in the future), thus you'll never end up with a retain cycle. So using weak in this case, then, is to prevent #2 from happening.

As Hamish mentions, weak is not actually needed in this example to prevent retain cycles, as there are no retain cycles. weak, in this case, is used to prevent an object living longer than needed. It depends entirely on your use-case as to when you consider an object living longer than needed. Thus there are times when you would want to use weak only outside (EX2), and other times when you would want to use the weak outer, strong inner, weak inner dance (EX3), for example.

More on #1 (preventing retain cycles)

To examine the retain cycle problem, let's say a function is storing a reference to the block (i.e. heap) instead of referencing the function directly (i.e. stack). Many times we don't know the internals of a class/function, so it's safer to assume that the function is retaining the block.

Now you can easily create a retain cycle using weak outer, and only using strong inner (EX3_B):

public class CaptureListExperiment {

    public init() {

    }

    var _someFunctionWithTrailingClosure: (() -> ())?
    var _anotherFunctionWithTrailingClosure: (() -> ())?

    func someFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting someFunctionWithTrailingClosure")
        _someFunctionWithTrailingClosure = closure

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
            self?._someFunctionWithTrailingClosure!()
            print("finishing someFunctionWithTrailingClosure")
        }
    }

    func anotherFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting anotherFunctionWithTrailingClosure")
        _anotherFunctionWithTrailingClosure = closure

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
            self?._anotherFunctionWithTrailingClosure!()
            print("finishing anotherFunctionWithTrailingClosure")
        }
    }

    func doSomething() {
        print("doSomething")
    }

    public func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            guard let self = self else { return }
            self.anotherFunctionWithTrailingClosure { // [weak self] in
                self.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

func performExperiment() {

    let obj = CaptureListExperiment()

    obj.testCompletionHandlers()
    Thread.sleep(forTimeInterval: 1.3)
}

performExperiment()

/* Output:

starting someFunctionWithTrailingClosure
starting anotherFunctionWithTrailingClosure
finishing someFunctionWithTrailingClosure
doSomething
finishing anotherFunctionWithTrailingClosure
*/

Notice that deinit is not called, since a retain cycle was created.

This could be fixed by either removing the strong reference (EX2):

public func testCompletionHandlers() {
    someFunctionWithTrailingClosure { [weak self] in
        //guard let self = self else { return }
        self?.anotherFunctionWithTrailingClosure { // [weak self] in
            self?.doSomething()
        }
    }
}

Or using the weak/strong/weak dance (EX3):

public func testCompletionHandlers() {
    someFunctionWithTrailingClosure { [weak self] in
        guard let self = self else { return }
        self.anotherFunctionWithTrailingClosure { [weak self] in
            self?.doSomething()
        }
    }
}

Updated for Swift 4.2:

public class CaptureListExperiment {

    public init() {

    }

    func someFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting someFunctionWithTrailingClosure")

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing someFunctionWithTrailingClosure")
        }
    }

    func anotherFunctionWithTrailingClosure(closure: @escaping () -> ()) {
        print("starting anotherFunctionWithTrailingClosure")

        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            closure()
            print("finishing anotherFunctionWithTrailingClosure")
        }
    }

    func doSomething() {
        print("doSomething")
    }

    public func testCompletionHandlers() {
        someFunctionWithTrailingClosure { [weak self] in
            guard let self = self else { return }
            self.anotherFunctionWithTrailingClosure { // [weak self] in
                self.doSomething()
            }
        }
    }

    // go ahead and add `deinit`, so I can see when this is deallocated

    deinit {
        print("deinit")
    }
}

try it Playgorund:

func performExperiment() {

    let obj = CaptureListExperiment()

    obj.testCompletionHandlers()
    Thread.sleep(forTimeInterval: 1.3)
}

performExperiment()