1

Here is my method to fetch some data from the network:

func fetchProducts(parameters: [String: Any],
                success: @escaping ([Product]) -> Void) 

As you noticed, it has escaping closure. Here is how I call above method in ViewModel:

service.fetchProducts(parameters: params, success: { response in
        self.isLoading?(false)
        /// doing something with response
}) 

The question is should I capture self weakly or strongly? Why? I think I can capture it strongly. Because, fetchProducts is a function which has closure as a parameter. But, I might be wrong. But, from other perspective, I think it should be weak. Because, ViewModel has strong reference to service, service has strong reference to success closure which has strong reference to self (which is ViewModel). It creates retain cycle. But deinit of ViewModel is called anyway, after ViewController which owns ViewModel is deinitialized. It means that there was no retain cycle. Why?

neo
  • 1,314
  • 2
  • 14
  • 34

1 Answers1

2

As long as your viewmodel is a class, you have to capture self weakly, otherwise you'll have a strong reference cycle. Since fetchProducts is asynchronous, its success closure might be executed after your viewmodel has already been deallocated - or would have been deallocated if the closure wasn't holding a strong reference to it. The strong reference in the async closure will block the viewmodel from being deallocated.

If you call service.fetchProducts in a class AND access self inside the async closure, you do need [weak self]. If you were to do this in a value type (struct or enum) OR if you didn't access self inside the closure, you don't need [weak self] - in a value type, you cannot even do [weak self].

service.fetchProducts(parameters: params, success: { [weak self] response in
        self?.isLoading?(false)
        /// doing something with response
}) 
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • Yes, ViewModel is reference type. But, how would you explain that deinit of ViewModel is called even there was strong self? – neo Sep 09 '20 at 14:19
  • 1
    @neo whether the strong reference cycle will be created or not, depends on when the closure is executed and when all other references to your viewmodel are deallocated. The strong reference cycle will only be closed if the last reference is inside the closure. So for instance if your network request took 10 seconds to come back with a result, but your user already navigated away from the view storing your view model after 5 seconds, there would be a strong reference cycle due to using strong self. You can guard against this with capturing self weakly. – Dávid Pásztor Sep 09 '20 at 14:24
  • 2
    Ok, but suppose my network request takes 10 seconds. User navigated out from that screen. Of course, my ViewModel cannot be deinitted. But after `success` closure gets executed, it will be deallocated. Where am I wrong? – neo Sep 09 '20 at 14:29
  • @DávidPásztor _So for instance if your network request took 10 seconds.._ this is not totally correct, I have exactly this case and when network is finished and I got response, everything is deallocated successfully. – vpoltave Sep 09 '20 at 14:32
  • @neo, no it won't be deallocated even after the request returned. That's where you are wrong. The memory leak is created because the viewmodel should've been deallocated as soon as all references outside of that closure were deallocated. However, the closure holding a reference to the viewmodel creates a zombie object that won't be deallocated. – Dávid Pásztor Sep 09 '20 at 14:33
  • If it is still unclear how closures can create strong reference cycles, I'd suggest reading some external materials, like [How to prevent memory leaks in Swift closures](https://stablekernel.com/article/how-to-prevent-memory-leaks-in-swift-closures/) or [How to prevent memory leak with using self in closure](https://stackoverflow.com/a/54431490/4667835). – Dávid Pásztor Sep 09 '20 at 14:37
  • Ok. I believe you are right, but I cannot check that ViewModel is not deallocated after request returned. Inside my `fetchProducts`, I created delay for 10 seconds using `DispatchQueue.main.asyncAfter`. Then, I called `fetchProducts` and navigated back to previous ViewController. Of course, ViewModel and a `ViewController` holding it, was not deallocated. But after as soon as `success` closure finished, they were deallocated. Thanks for links by the way! – neo Sep 09 '20 at 14:43