Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI NavigationLink memory leak

I have a question on how memory management works in SwiftUI's NavigationView stack. I have a view, in which I have declared NavigationView and NavigationLink, inside destination parameter of NavigationLink is my TestView. Navigation works good, but when I pop view from stack (f. e. up back button) deinit is not printed in console and TestViewModel can still be found in memory graph. How do I deinitialize my TestViewModel, when it's not needed anymore ?

    /// First view in application
    struct ContentView: View {

        var body: some View {
            NavigationView {
                VStack {
                    Text("Hello, leak!")
                    NavigationLink(
                        destination: TestView(viewModel: TestViewModel()),
                        label: { Text("Create leak 🤷‍♂️") }
                    )
                }
            }
        }
    }

    /// Just simple class for init and deinit print
    class TestViewModel: ObservableObject {

        @Published var text = "Test"

        init() {
            print("TestViewModel init")
        }

        deinit {
            print("TestViewModel deinit")
        }
    }

    /// Second view, which is poped from stack
    private struct TestView: View {

        @ObservedObject var viewModel: TestViewModel

        var body: some View {
            Text(viewModel.text)
        }
    }

UPDATE Added memory graph screenshot, which I preveiosly forget.

Memory graph screenshot bottom part

Memory graph screenshot top part

UPDATE

Tested on real device, where navigation works. It looks like, view model is not deinitializad, when poping view, but initialized again, when pushing another time. But question still remains, is there a way to deinit view model, when popping view in navigation stack ?

TestViewModel init
TestViewModel deinit
TestViewModel init

Also, when I add another view to stack, behaviour changes a bit. Now second's view view model will cause leak, but first will be deinitialized as expected.

First view push
TestViewModel init
Second view push
TestViewModel2 init
Second view pop
First view pop
TestViewModel deinit
like image 933
Róbert Oravec Avatar asked Feb 08 '20 17:02

Róbert Oravec


3 Answers

I had the same problem and spent a lot of time to figure it out. Finally, I got it! Use .navigationViewStyle(StackNavigationViewStyle()). Add is as a function to NavigationView:

NavigationView {
   ...
}
.navigationViewStyle(StackNavigationViewStyle())
like image 143
Dmytro Ripa Avatar answered Nov 20 '22 07:11

Dmytro Ripa


As I am watching Data Essentials in SwiftUI I think, I found the answer to my question. It's new StateObject property wrapper (I could not find the documentation, but here is post which describes it). Now, I can use @StateObject, when I want my data to only exists inside a view scope, whithout doing any hacks.

like image 6
Róbert Oravec Avatar answered Nov 20 '22 08:11

Róbert Oravec


This is happening because the default navigation style is ColumnNavigationViewStyle, which would show the navigation list AND the selected detail item any time the horizontal size class is regular.

To see this in action run the app on a iPhone Pro Max. Then select an item in portrait, go back, and then rotate the device. Bingo. You'll see the selected item in the second column.

So it turns out that in order to make this magic happen ColumnNavigationViewStyle will remember the selected item until a new item item is selected.

Which in turn gives you the mysterious "retain cycle". It's not a leak, it's just how it works. (Even on devices like the iPhone mini that will never allow the second column to appear.)

The .navigationViewStyle(StackNavigationViewStyle()) fix mentioned elsewhere changes this behavior.

like image 1
Michael Long Avatar answered Nov 20 '22 07:11

Michael Long