Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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")
                }

            )
        }
    }
}
like image 439
Vahagn Gevorgyan Avatar asked Apr 19 '20 11:04

Vahagn Gevorgyan


2 Answers

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())
like image 123
Tiziano Coroneo Avatar answered Oct 18 '22 00:10

Tiziano Coroneo


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)
    })
}
like image 3
saniaxxx26 Avatar answered Oct 18 '22 00:10

saniaxxx26