You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何向Sheet View构造器传入ViewModel会导致内存泄漏?

问题:Sheet ViewModel 传入构造器后关闭Sheet时无法执行deinit

我的目标是遵循依赖倒置原则,让SheetView依赖于Sheet ViewModel的协议。但遇到问题:将ViewModel传入Sheet View的构造器后,关闭Sheet View时ViewModel不会执行deinit。

代码示例:

.sheet(isPresented: $viewModel.isSheetPresented) {
    let params = SheetViewModelParams(
        initialCount: 0,
        initialViewType: .List
    )
    
    /**
     - Bug: ViewModel will not deinit. Use .init(params:)
     */
//    let viewModel = SheetViewModel(params: params)
//    SheetView(viewModel: viewModel)
    
    SheetView(params: params)
}

测试要求

  • 仅在Sheet View展示时初始化Sheet ViewModel;
  • 关闭Sheet View时销毁Sheet ViewModel;
  • 展示Sheet List View时初始化Sheet List ViewModel;
  • 关闭Sheet View或展示Sheet New View时销毁Sheet List ViewModel。

核心代码文件

ContentWithoutStateView.swift

import SwiftUI

struct ContentWithoutStateView: View {
    @State var renderCount: Int = 0
    
    @StateObject var viewModel: ContentWithoutStateViewModel
    
    init(params: ContentWithoutStateViewModelParams) {
        self._viewModel = StateObject(
            wrappedValue: ContentWithoutStateViewModel.shared(params: params)
        )
    }
    
    var body: some View {
        VStack(spacing: 8) {
            Text("Content Without State")
            
            switch viewModel.onSubmitStatus {
            case .initial:
                Button {
                    Task {
                        await viewModel.onSubmit()
                    }
                } label: {
                    Text("Submit")
                }
                .onAppear {
                    print("\(type(of: self)) initial")
                }
            case .loading:
                ProgressView()
                    .onAppear {
                        print("\(type(of: self)) loading")
                    }
            case .success:
                Text("Success")
                    .onAppear {
                        print("\(type(of: self)) Success")
                    }
            case .failure:
                Text("Failure")
                    .onAppear {
                        print("\(type(of: self)) Failure")
                    }
            }
            
            CountComponent(
                count: $viewModel.count,
                onDecrement: {
                    viewModel.count -= 1
                },
                onIncrement: {
                    viewModel.count += 1
                }
            )
            
            Button {
                viewModel.isSheetPresented = true
            } label: {
                Text("show the sheet")
            }
        }
        .padding(.all, 16)
        .border(.secondary)
        .onReceive(viewModel.objectWillChange, perform: { _ in
            renderCount += 1
            print("\(type(of: self)) viewModel will change. count: \(renderCount)")
        })
        .sheet(isPresented: $viewModel.isSheetPresented) {
            let params = SheetViewModelParams(
                initialCount: 0,
                initialViewType: .List
            )
            
            /**
             - Bug: ViewModel will not deinit. Use .init(params:)
             */
//            let viewModel = SheetViewModel(params: params)
//            SheetView(viewModel: viewModel)
            
            SheetView(params: params)
        }
    }
}

#Preview {
    let params = ContentWithoutStateViewModelParams()
    return ContentWithoutStateView(params: params)
}

OnSubmitStatus.swift

enum OnSubmitStatus {
    case initial
    case loading
    case success
    case failure
}

ContentWithoutStateViewModelParams.swift

struct ContentWithoutStateViewModelParams {
    let initialCount: Int
    let initialOnSubmitStatus: OnSubmitStatus
    let initialIsSheetPresented: Bool
    
    init(
        initialCount: Int = 0,
        initialOnSubmitStatus: OnSubmitStatus = .initial,
        initialIsSheetPresented: Bool = false
    ) {
        self.initialCount = initialCount
        self.initialOnSubmitStatus = initialOnSubmitStatus
        self.initialIsSheetPresented = initialIsSheetPresented
    }
}

ContentWithoutStateViewModel.swift

final class ContentWithoutStateViewModel: ObservableObject {
    @Published var count: Int
    @Published var onSubmitStatus: OnSubmitStatus
    
    @Published var isSheetPresented: Bool
    
    init(params: ContentWithoutStateViewModelParams) {
        self.count = params.initialCount
        self.onSubmitStatus = params.initialOnSubmitStatus
        self.isSheetPresented = params.initialIsSheetPresented
        print("\(type(of: self)) \(#function)")
    }
    
    deinit {
        print("\(type(of: self)) \(#function)")
    }
    
    func fetchContent() async -> Result<Bool, Error> {
        sleep(1)
        return .success(true)
    }
    
    @MainActor
    func onSubmit() async {
        onSubmitStatus = .loading
        
        let result = await fetchContent()
        
        result.fold { success in
            count += 1
            onSubmitStatus = .success
        } errorTransform: { failure in
            count -= 1
            onSubmitStatus = .failure
        }

    }
}

ContentWithoutStateViewModel+Shared.swift

extension ContentWithoutStateViewModel {
    static func shared(params: ContentWithoutStateViewModelParams) -> ContentWithoutStateViewModel {
        var temp: ContentWithoutStateViewModel
        
        if _shared == nil {
            temp = ContentWithoutStateViewModel(params: params)
            _shared = temp
        }
        
        return _shared!
    }
    
    static weak var _shared: ContentWithoutStateViewModel?
}

Sheet 模块代码

SheetView.swift

struct SheetView: View {
    @State var renderCount: Int = 0
    
    @StateObject var viewModel: SheetViewModel
    
    init(params: SheetViewModelParams) {
        self._viewModel = StateObject(
            wrappedValue: SheetViewModel.shared(params: params)
        )
    }
    
    @available(
        *,
         deprecated,
         message: "Bug: ViewModel will not deinit when Sheet is dismissed. use .init(params:)"
    )
    init(viewModel: SheetViewModel) {
        self._viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var body: some View {
        VStack(spacing: 8) {
            Text("Sheet")
            
            CountComponent(
                count: $viewModel.count,
                onDecrement: {
                    viewModel.count -= 1
                },
                onIncrement: {
                    viewModel.count += 1
                }
            )
            
            Button {
                viewModel.selectedViewType = .List
            } label: {
                Text("show the sheet list")
            }
            
            Button {
                viewModel.selectedViewType = .New
            } label: {
                Text("show the sheet new")
            }
            
            switch viewModel.selectedViewType {
            case .List:
                let params = SheetListViewModelParams(initialCount: 0)
                SheetListView(params: params)
            case .New:
                let params = SheetNewViewModelParams(initialCount: 0)
                SheetNewView(params: params)
            }
        }
        .padding(.all, 16)
        .border(.secondary)
        .onReceive(viewModel.objectWillChange, perform: { _ in
            renderCount += 1
            print("\(type(of: self)) viewModel will change. count: \(renderCount)")
        })
    }
}

#Preview {
    let params = SheetViewModelParams(
        initialCount: 0, 
        initialViewType: .List
    )
    return SheetView(params: params)
}

SheetViewType.swift

enum SheetViewType {
    case List
    case New
}

SheetViewModelParams.swift

struct SheetViewModelParams {
    let initialCount: Int
    let initialViewType: SheetViewType
}

SheetViewModel.swift

import Foundation

final class SheetViewModel: ObservableObject {
    let id: UUID = UUID()
    
    @Published var count: Int
    @Published var selectedViewType: SheetViewType
    
    init(
        params: SheetViewModelParams
    ) {
        self.count = params.initialCount
        self.selectedViewType = params.initialViewType
        print("\(type(of: self)) \(#function) \(id)")
    }
    
    deinit {
        print("\(type(of: self)) \(#function) \(id)")
    }
}

SheetViewModel+Shared.swift

extension SheetViewModel {
    static func shared(params: SheetViewModelParams) -> SheetViewModel {
        var temp: SheetViewModel
        
        if _shared == nil {
            temp = SheetViewModel(params: params)
            _shared = temp
        }
        
        return _shared!
    }
    
    private static weak var _shared: SheetViewModel?
}

Sheet List 模块代码

SheetListView.swift

struct SheetListView: View {
    @State var renderCount: Int = 0
    
    @StateObject var viewModel: SheetListViewModel
    
    init(params: SheetListViewModelParams) {
        self._viewModel = StateObject(
            wrappedValue: SheetListViewModel.shared(params: params)
        )
    }
    
    var body: some View {
        VStack(spacing: 8) {
            Text("Sheet List")
            
            CountComponent(
                count: $viewModel.count,
                onDecrement: {
                    viewModel.count -= 1
                },
                onIncrement: {
                    viewModel.count += 1
                }
            )
        }
        .padding(.all, 16)
        .border(.secondary)
        .onReceive(viewModel.objectWillChange, perform: { _ in
            renderCount += 1
            print("\(type(of: self)) viewModel will change. count: \(renderCount)")
        })
    }
}

#Preview {
    let params = SheetListViewModelParams(initialCount: 0)
    return SheetListView(params: params)
}

SheetListViewModelParams.swift

import Foundation

struct SheetListViewModelParams {
    let initialCount: Int
}

SheetListViewModel.swift

import Foundation

final class SheetListViewModel: ObservableObject {
    let id = UUID()
    
    @Published var count: Int
    
    init(params: SheetListViewModelParams) {
        self.count = params.initialCount
        
        print("\(type(of: self)) \(#function) \(id)")
    }
    
    deinit {
        print("\(type(of: self)) \(#function) \(id)")
    }
}

SheetListViewModel+Shared.swift

extension SheetListViewModel {
    static func shared(params: SheetListViewModelParams) -> SheetListViewModel {
        var temp: SheetListViewModel
        
        if _shared == nil {
            temp = SheetListViewModel(params: params)
            _shared = temp
        }
        
        return _shared!
    }
    
    private static weak var _shared: SheetListViewModel?
}

Sheet New 模块代码

SheetNewView.swift

import SwiftUI

struct SheetNewView: View {
    @State var renderCount = 0
    
    @StateObject var viewModel: SheetNewViewModel
    
    init(params: SheetNewViewModelParams) {
        self._viewModel = StateObject(
            wrappedValue: SheetNewViewModel.shared(params: params)
        )
    }
    
    var body: some View {
        VStack(spacing: 8) {
            Text("Sheet New")
            
            CountComponent(
                count: $viewModel.count,
                onDecrement: {
                    viewModel.count -= 1
                },
                onIncrement: {
                    viewModel.count += 1
                }
            )
        }
        .padding(.all, 16)
        .border(.secondary)
        .onReceive(viewModel.objectWillChange, perform: { _ in
            renderCount += 1
            print("\(type(of: self)) viewModel will change. count: \(renderCount)")
        })
    }
}

#Preview {
    let params = SheetNewViewModelParams(initialCount: 0)
    return SheetNewView(params: params)
}

SheetNewViewModelParams.swift

struct SheetNewViewModelParams {
    let initialCount: Int
}

SheetNewViewModel.swift

import Foundation

final class SheetNewViewModel: ObservableObject {
    let id: UUID = UUID()
    
    @Published var count: Int
    
    init(params: SheetNewViewModelParams) {
        self.count = params.initialCount
        print("\(type(of: self)) \(#function) \(id)")
    }
    
    deinit {
        print("\(type(of: self)) \(#function) \(id)")
    }
}

SheetNewViewModel+Shared.swift

extension SheetNewViewModel {
    static func shared(params: SheetNewViewModelParams) -> SheetNewViewModel {
        var temp: SheetNewViewModel
    
        if _shared == nil {
            temp = SheetNewViewModel(params: params)
            _shared = temp
        }
    
        return _shared!
    }
    
    private static weak var _shared: SheetNewViewModel!
}

内容的提问来源于stack exchange,提问作者Jason Rich Darmawan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 12:07:39