zaitsman
zaitsman

Reputation: 9509

Should we continue explicitly capturing variable as weak in iOS

Suppose we got a chain of closures, like so:

var myOtherVc: UIViewController! // get it somehow
self.dismiss(animated: true, completion: { [weak myOtherVc] in 
   myOtherVc?.present(sthElse, animated: true, completion: { [weak myOtherVc] in // <-- HERE
   })
})

My question is if we captured variable myOtherVc in the topmost block as weak should we keep being explicit about weak in all the children blocks or is compiler smart enough to tell ARC to not retain?

Update

I guess I need to clarify, what if the block was escaping? Also, i DO care about delayed deallocation. This is the whole point of using weak for me.

public func doWhatever(_ success: @escaping () -> Void) { 
// do whatever
})

var myOtherVc: UIViewController! // get it somehow
self.dismiss(animated: true, completion: { [weak myOtherVc] in 
SomeClass.doWhatever({ [weak myOtherVc] in // <-- HERE
     myOtherVc?.present(sthElse, animated: true, completion: { [weak myOtherVc] in // <-- and HERE, too
     })
  })
})

Upvotes: 4

Views: 426

Answers (3)

Mahsa Yousefi
Mahsa Yousefi

Reputation: 236

We suppose all closures are escaping, so I made this playground and I conclude that you must capture your variable as weak in the latest closure that is using your variable and it won't infer its reference type from the parent or top closure:

typealias Closure = () -> Void
class A {
    var closureA : Closure?

    func runClosureA(closure: @escaping Closure) {
        self.closureA = closure
        closureA?()
    }

    func print() {
        debugPrint("A is here!")
    }

    deinit {
        debugPrint("A deinited")
    }
}

another class which operates on chained closure:

class B {


func runClosureB(closure: @escaping Closure) {
    closure()
}

func operate() {
    let a : A = A()
    runClosureB { [weak a] in
        a?.runClosureA { [a] in
            a?.print()
        }
    }
}

deinit {
    debugPrint("B deinited")
}
}

The code is:

var b: B? = B()
b?.operate()
b = nil

It will prints:

// "A is here!"
// "B deinited"

But by changing the operate function to this:

func operate() {
    let a : A = A()
    runClosureB { [a] in
        a.runClosureA { [weak a] in
            a?.print()
        }
    }
}

The result will change to this:

"A is here!"
"A deinited"
"B deinited"

Update: In class A I made a strong reference to the closure as closureA, if you don't create the reference, there is no need to capture self as weak in closures.

In fact, it depends on which closure you are using and the relations between them and if there can be retained cycle so you should consider capturing the right closure as weak.

Upvotes: 1

Duver Arthur
Duver Arthur

Reputation: 98

I made a small test in the playground that shows me that the compiler is indeed quite smart and tells ARC not to retain.

class WeakThingy {
    var variable: Int
    
    init(variable: Int) {
        self.variable = variable
    }
    
    deinit {
        print("deinit WeakThingy")
    }
}

class ClosureTest {
    var maybeNil: WeakThingy
    
    init(maybeNil: WeakThingy) {
        self.maybeNil = maybeNil
    }
    
    deinit {
        print("deinit ClosureTest")
    }
    
    func bien() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak maybeNil] in
            print("first \(String(describing: maybeNil))")
            maybeNil?.variable = 12
            DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
                print("second \(String(describing: maybeNil))")
                maybeNil?.variable = 12
            }
        }
    }
}

var closureTest:ClosureTest? = ClosureTest(maybeNil: WeakThingy(variable: 12))

closureTest?.bien()

DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    closureTest = nil
}

What's printed is the following

first Optional(__lldb_expr_34.WeakThingy)

deinit ClosureTest

deinit WeakThingy

second nil

What happen here is that I pass maybeNil in the first closure as weak, but not in the second. Then I make it nil before the second closure gets executed. We can see in the output that it is well deallocated before entering the second closure.

Upvotes: 1

emrcftci
emrcftci

Reputation: 3514

In your case, with present(_, animated:, completion:), the completion block is non-escaping so if you want to use weak reference you can use it but it is not necessary to use.

Non-escaping closures do not require [weak self] unless you care about delayed deallocation

flowchart

Please check the article about weak, unowned references in nested closures.

Upvotes: 1

Related Questions