简体   繁体   中英

SwiftUI - memory leak in NavigationView

I am trying to add a close button to the modally presented View's navigation bar. However, after dismiss, my view models deinit method is never called. I've found that the problem is where it captures the self in navigationBarItem 's. I can't just pass a weak self in navigationBarItem 's action, because View is a struct, not a class. Is this a valid issue or just a lack of knowledge?

struct ModalView: View {

    @Environment(\.presentationMode) private var presentation: Binding<PresentationMode>
    @ObservedObject var viewModel: ViewModel

    var body: some View {

        NavigationView {
            Text("Modal is presented")
            .navigationBarItems(leading:
                Button(action: {
                    // works after commenting this line
                    self.presentation.wrappedValue.dismiss()
                }) {
                    Text("close")
                }

            )
        }
    }
}

You don't need to split the close button out in its own view. You can solve this memory leak by adding a capture list to the NavigationView's closure: this will break the reference cycle that retains your viewModel .

You can copy/paste this sample code in a playground to see that it solves the issue (Xcode 11.4.1, iOS playground).

import SwiftUI
import PlaygroundSupport

struct ModalView: View {
    @Environment(\.presentationMode) private var presentation
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        // Capturing only the `presentation` property to avoid retaining `self`, since `self` would also retain `viewModel`.
        // Without this capture list (`->` means `retains`):
        // self -> body -> NavigationView -> Button -> action -> self
        // this is a retain cycle, and since `self` also retains `viewModel`, it's never deallocated.
        NavigationView { [presentation] in
            Text("Modal is presented")
                .navigationBarItems(leading: Button(
                    action: {
                        // Using `presentation` without `self`
                        presentation.wrappedValue.dismiss()
                },
                    label: { Text("close") }))
        }
    }
}

class ViewModel: ObservableObject { // << tested view model
    init() {
        print(">> inited")
    }

    deinit {
        print("[x] destroyed")
    }
}

struct TestNavigationMemoryLeak: View {
    @State private var showModal = false
    var body: some View {
        Button("Show") { self.showModal.toggle() }
            .sheet(isPresented: $showModal) { ModalView(viewModel: ViewModel()) }
    }
}

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(TestNavigationMemoryLeak())

I recommend design-level solution, ie. decomposing navigation bar item into separate view component breaks that undesired cycle referencing that result in leak.

Tested with Xcode 11.4 / iOS 13.4 - ViewModel destroyed as expected.

Here is complete test module code:

struct CloseBarItem: View { // separated bar item with passed binding
    @Binding var presentation: PresentationMode
    var body: some View {
        Button(action: {
            self.presentation.dismiss()
        }) {
            Text("close")
        }
    }
}

struct ModalView: View {
    @Environment(\.presentationMode) private var presentation
    @ObservedObject var viewModel: ViewModel

    var body: some View {

        NavigationView {
            Text("Modal is presented")
            .navigationBarItems(leading: 
                CloseBarItem(presentation: presentation)) // << decompose
        }
    }
}

class ViewModel: ObservableObject {    // << tested view model
    init() {
        print(">> inited")
    }

    deinit {
        print("[x] destroyed")
    }
}

struct TestNavigationMemoryLeak: View {
    @State private var showModal = false
    var body: some View {
        Button("Show") { self.showModal.toggle() }
            .sheet(isPresented: $showModal) { ModalView(viewModel: ViewModel()) }
    }
}

struct TestNavigationMemoryLeak_Previews: PreviewProvider {
    static var previews: some View {
        TestNavigationMemoryLeak()
    }
}

backup

My solution is

.navigationBarItems(
    trailing: self.filterButton
)

..........................................

var filterButton: some View {
    Button(action: {[weak viewModel] in
        viewModel?.showFilter()
    },label: {
        Image("search-filter-icon").renderingMode(.original)
    })
}

I was having a gnarly memory leak due to navigationBarItems and passing my view model to the view I was using as the bar item.

Digging around on this, I learned that navigationBarItems is deprecated

I had

        .navigationBarItems(trailing:
            AlbumItemsScreenNavButtons(viewModel: viewModel)
        )

The replacement is toolbar .

My usage now looks like this:

        .toolbar {
            ToolbarItemGroup(placement: .navigationBarTrailing) {
                AlbumItemsScreenNavButtons(viewModel: viewModel)
            }
        }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM