10

I have something like this:

struct SomeView: View {
  @ObservedObject var viewModel: SomeViewModel

  var body: some View {
     NavigationView { // <- culprit
        Button(action: { self.viewModel.logOut() }) { Text("X").frame(width: 40, height: 40) }
     }
}

class SomeViewModel: ObservableObject {
  func logOut() {
  // changes global state, based on which the views are swapped, so `SomeView` is removed and replaced by a different one
  }
}

When the button is pressed, SomeView is closed and a different view is presented. But if I check the memory graph, SomeViewModel is still allocated because self.viewModel.logOut() is called in the Button's action closure and Button is holding the reference to SomeViewModel.

Is there some way around this?

EDIT: Actually, when not not wrapping the button in NavigationView, there is no leak. As soon as I wrap the button, the leak appears. Wrapping in VStack is working out fine. But wrapping in Form produces the leak again. Seems like the same problem here: SwiftUI - Possible Memory Leak

NeverwinterMoon
  • 2,363
  • 21
  • 24
  • Have you tried to call in closure something like `self.logOut()`, which would call inside `self.viewModel.logOut()`. Just interesting what would be the difference. – Asperi Dec 12 '19 at 14:56
  • Yes, I did try that and some other things - nothing worked. Only not using NavigationView/List/Form around the button does not create a leak... but that doesn't help me at all. – NeverwinterMoon Dec 12 '19 at 16:21
  • could you call ` self.viewModel.logOut()` in onDisappear{} and the button just for pop or dismiss the `SomeView` – E.Coms Dec 12 '19 at 18:42
  • 1
    When you say there's a leak, is this a persistent leak that grows with every interaction? Or is it something that occurs once? How many bytes are being leaked? Small leaks (16 bytes are the most common) are completely normal in UIKit. UIKit itself has memory leaks. – Rob Napier Dec 12 '19 at 18:42
  • @Rob Napier Every time I go back and forth, the new ViewModel is created and the old one remains in memory. In my real code, every model also has its own service and that service also remains in the memory. It keeps spawning and spawning them. So it’s not insignificant. – NeverwinterMoon Dec 12 '19 at 20:11
  • I have something similar. view model leaks if used in `navigationBarItems(trailing: Button(action: { ... }))`. It's fine as long as it's not in `navigationBarItems`. – User Sep 21 '20 at 18:28

1 Answers1

11

I found a solution: Make a weak viewModel in your action. It seems that Apple changed the behavior of closures. This means that the NavigationView is storing a strong reference to viewModel. After a couple days of debugging, it finally worked for me.

Button(action: { 
    [weak viewModel] in viewModel?.dismissButtonPressed.send(())
}) {
    Image("crossmark")
        .padding()
        .foregroundColor(Color.white)
    }
}

In your problem, this will be solved like this:

NavigationView { 
    [weak viewModel] in Button(action: { viewModel?.logOut() }) {
        Text("X").frame(width: 40, height: 40)
    }
}

Tested on the latest Xcode 11.5, with iOS 13.5. Now, after dismissing the view, the viewModel is correctly deallocated.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
Oleksandr Vaker
  • 162
  • 2
  • 8
  • 2
    Since the time I've been struggling with this, I've moved to a global state + actions that modify that state instead of using local view models. Actually, I even stopped using NavigationView, too, as it had some other issues, in favour of a custom routing handling. But this does look legit, so I'll mark it as the correct answer for now. Thanks! – NeverwinterMoon Jun 04 '20 at 10:03
  • Excellent! It really helped my case, where I had a `toolbar` with multiple buttons and a menu, all of them were accessing an `EnvironmentObject`. After I applied `unowned` in the capture list, my memory leaks were fixed! thank you! – Gal Yedidovich Jan 27 '21 at 09:31