This is a good question about how weak references interact with bound method references! The first thing to note is that when you form a bound reference to some instance method, you are implicitly creating a closure which captures the self param for the method. So in the following code:
class C {
var x: Int = 0
func f() { print(x) }
}
let c = C()
c.x = 3
let printX = c.f
printX() // 3
the line let printX = c.f is doing basically the same thing as if you had written:
let printX = { [c] in
c.f()
}
If you think about what would happen after c goes out of scope, you can see why this has to be the case: printX is able to be passed around independently of c, and if it didn't have its own (strong) reference to the underlying instance, it would be possible for c to be released and deallocated out from underneath printX, which would suddenly present a memory safety issue (because printX purports to call the C.f method on an instance that no longer exists.
But why does this still happen in the TestViewModel case when we have a weak reference? The thing to notice here is that while we may have a weak reference externally, within TestViewModel all methods work with a strong reference to self. That is, for the duration of the execution of the method, it is not possible for self to disappear out from under us—it will only be possible for self to be deallocated after the method completes execution and the self parameter is released.
So in the case of a bound method reference, if we're able to form a reference to TestViewModel.someAsyncAction that can be invoked later, we must have a strong reference to TestViewModel to pass as the self parameter. By forming that method reference through our weak instance reference, we are attempting to form the strong reference at the time we form the onAsyncAction closure. The code you've written in the first example is roughly semantically equivalent to the following:
let onAsyncAction = if let viewModel = viewModel {
viewModel.someAsyncAction
} else {
nil
}
testStruct.saveActionGlobally(onAsyncAction: onAsyncAction)
By doing things this way, we have formed a function which captures a strong reference to the TestViewModel instance for the duration of the lifetime of the underlying function, so we cannot deallocate viewModel until after onAsyncAction is deallocated.
OTOH, in the second example, we are forming a closure which captures only captures a weak reference to TestViewModel. Yes, we still have to form a strong reference in order to invoke someAsyncAction, but the key here is that this strong reference is formed only at the time of execution of onAsyncAction, and only for the duration of execution of someAsyncAction. The more explicit semantic equivalent looks something like this:
testStruct.saveActionGlobaly(onAsyncAction: {
if let viewModel = viewModel {
viewModel.someAsyncAction()
}
})
Hopefully this version makes it a bit clearer that we are only forming the strong reference to TestViewModel within the closure passed to saveActionGlobally, and that this reference gets released once we exit the scope, allowing the TestViewModel instance to be released if there are only weak references remaining.