SwiftUI - NavigationView 中的内存泄漏

Posted

技术标签:

【中文标题】SwiftUI - NavigationView 中的内存泄漏【英文标题】:SwiftUI - memory leak in NavigationView 【发布时间】:2020-08-01 20:01:24 【问题描述】:

我正在尝试向模态显示的视图导航栏添加一个关闭按钮。但是,解雇后,我的视图模型 deinit 方法永远不会被调用。我发现问题在于它在 navigationBarItem 中捕获了 self。我不能只在 navigationBarItem 的操作中传递weak self,因为 View 是一个结构,而不是一个类。这是一个有效的问题还是只是缺乏知识?

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

            )
        
    

【问题讨论】:

【参考方案1】:

由于navigationBarItems,我遇到了严重的内存泄漏,并将我的视图模型传递给我用作条形项目的视图。

四处挖掘,我了解到navigationBarItems is deprecated

我有

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

替换为toolbar

我现在的用法是这样的:

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

【讨论】:

【参考方案2】:

我的解决办法是

.navigationBarItems(
    trailing: self.filterButton
)

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

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

【讨论】:

【参考方案3】:

您不需要在自己的视图中拆分关闭按钮。您可以通过在 NavigationView 的闭包中添加 capture list 来解决此内存泄漏问题:这将破坏保留您的 viewModel 的引用循环。

您可以将此示例代码复制/粘贴到 Playground 中,看看它是否解决了问题(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())

【讨论】:

【参考方案4】:

我推荐设计级解决方案,即。将导航栏项目分解为单独的视图组件会破坏导致泄漏的不希望的循环引用。

使用 Xcode 11.4 / iOS 13.4 测试 - ViewModel 按预期销毁。

这里是完整的测试模块代码:

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()
    

【讨论】:

以上是关于SwiftUI - NavigationView 中的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

NavigationView高度不规则swiftui

SwiftUI NavigationView 不记得状态

SwiftUI 弹出框在 NavigationView 中时消失

SwiftUI:检测 tvOS 上 NavigationView 中的选择更改

在 NavigationView 中清除 SwiftUI 列表未正确返回默认值

如何在启动时显示 NavigationView (SwiftUI) 的详细视图