Edit (after discussion):
It's a common practice in SwiftUI
to have a View (struct)
hold an ObservableObject
. Since a struct
is not a reference type, it's easy to forget that is holds one.
The questions should be really:
- Can an indirect references cause a retain cycle?
- Can a retain cycle involve a single pointer
The answer to both is yes.
- struct -> class -> closure{struct.class}
class ViewModel: ObservableObject
...
struct MyView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
Button("Action") {
viewModel?.handler {
// ViewModel is retained here through self:
// (MyView.viewModel)
self.someAction()
}
}
}
}
In SwiftUI
, it's not a good practice to 'pass yourself' to your ViewModel
- Class holding an instance var pointing to itself
(discussed here How does ARC deal with circular linked lists when the external head reference is removed?)
class A {
var next: A
}
let a = A()
a.next = a // Retain cycle of an object to itself.
(Original question)
Why is holding a reference to a closure cause a memory leak even if the closure does not retain anything?
(This started with a SwiftUI
issue, but I'm not sure it's related really)
So here's a simple Manager (ViewModel
) that holds a closure.
class Manager: ObservableObject {
private var handler: (() -> Void)?
deinit {
print("Manager deallocated")
}
func shouldDismiss(completion: @escaping () -> Void) {
handler = completion
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.handler?()
}
}
}
Here's a view, using it:
struct LeakingView: View {
@ObservedObject var manager: Manager
let shouldDismiss: () -> Void
var body: some View {
Button("Dismiss") { [weak manager] in
manager?.shouldDismiss {
self.shouldDismiss()
}
}
}
}
Here's a ViewController
activating the flow:
class ViewController: UIViewController {
var popup: UIViewController?
@IBAction func openViewAction(_ sender: UIButton) {
let manager = Manager()
let suiView = LeakingView(manager: manager) { [weak self] in
self?.popup?.view.removeFromSuperview()
self?.popup?.dismiss(animated: true)
self?.popup = nil
}
popup = LKHostingView(rootView: suiView)
popup?.view.frame.size = CGSize(width: 300, height: 300)
view.addSubview(popup!.view)
}
}
When running this, as long as the manager is holding the closure, it will leak and the Manager
will not get released.
Where is the retain cycle?