SwiftUI - memory leak in NavigationView

前端 未结 3 559
时光取名叫无心
时光取名叫无心 2021-01-05 07:35

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\'

相关标签:
3条回答
  • 2021-01-05 08:04

    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())
    
    0 讨论(0)
  • 2021-01-05 08:06

    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)
        })
    }
    
    0 讨论(0)
  • 2021-01-05 08:07

    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()
        }
    }
    
    0 讨论(0)
提交回复
热议问题