为什么在Sheet View的构造函数中传递ViewModel会导致内存泄漏?

问题描述 投票:0回答:1

我的目标是遵守依赖倒置原则。这意味着 SheetView 应该依赖于 Sheet ViewModel 的协议。

问题是当我将 ViewModel 传递给工作表视图的构造函数时,当我关闭工作表视图时,它不会取消初始化。

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

完整代码:swift-bloc-example

下面的代码测试:

  1. 急切加载工作表视图,只会在工作表视图呈现时初始化工作表视图模型。
  2. 当工作表视图被关闭时,deinit Sheet ViewModel。
  3. 当呈现图纸列表视图时初始化图纸列表视图模型。
  4. 当工作表视图或呈现工作表新视图时,删除工作表列表视图模型。

=== 此测试所需的代码。

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

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?
}

==== 片材

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.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?
}

=== 图纸列表

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.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?
}

=== 新表

SheetNew.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.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!
}
ios swift mvvm solid-principles
1个回答
0
投票

这似乎是苹果端的一个合法错误。将 viewModel 传递给 View 的构造函数并不会真正“导致”内存泄漏,但它允许内存泄漏发生。否则你所做的应该没问题:

  • SheetViewModel 是在闭包中创建的,因此它不应该作为 ContentWithoutStateView 主体的一部分进行计算。
  • 视图是一种值类型,无论如何都不应该增加引用计数。
  1. Apple 开发论坛上的相关问题: https://developer.apple.com/forums/thread/737967?answerId=767599022#767599022

这似乎是 iOS 17 上的一个已知问题(r.115856582),影响工作表和全屏演示文稿

  1. 这里有一个非常相似的问题: SwiftUI Sheet 永远不会从内存中释放对象
© www.soinside.com 2019 - 2024. All rights reserved.