20.3 C
London
Tuesday, September 17, 2024

ios – Why is passing ViewModel in a Sheet View’s constructor, it trigger reminiscence leak?


My purpose is to evolve to the Dependency Inversion precept. That means that SheetView ought to depend upon Sheet ViewModel’s protocol.

The issue is after I move ViewModel to a Sheet View’s constructor, after I dismiss the Sheet View, it won’t deinit.

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

Full code: swift-bloc-example

The code under take a look at:

  1. eagerly load the Sheet View, will solely initialize the Sheet ViewModel when the Sheet View is introduced.
  2. deinit Sheet ViewModel when the Sheet View is dismissed.
  3. init Sheet Listing ViewModel when the Sheet Listing View is introduced.
  4. deinit Sheet Listing ViewModel when the Sheet View or when it current Sheet New View.

=== Code that’s obligatory for this take a look at.

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 physique: some View {
        VStack(spacing: 8) {
            Textual content("Content material With out State")
            
            change viewModel.onSubmitStatus {
            case .preliminary:
                Button {
                    Job {
                        await viewModel.onSubmit()
                    }
                } label: {
                    Textual content("Submit")
                }
                .onAppear {
                    print("(kind(of: self)) preliminary")
                }
            case .loading:
                ProgressView()
                    .onAppear {
                        print("(kind(of: self)) loading")
                    }
            case .success:
                Textual content("Success")
                    .onAppear {
                        print("(kind(of: self)) Success")
                    }
            case .failure:
                Textual content("Failure")
                    .onAppear {
                        print("(kind(of: self)) Failure")
                    }
            }
            
            CountComponent(
                depend: $viewModel.depend,
                onDecrement: {
                    viewModel.depend -= 1
                },
                onIncrement: {
                    viewModel.depend += 1
                }
            )
            
            Button {
                viewModel.isSheetPresented = true
            } label: {
                Textual content("present the sheet")
            }
        }
        .padding(.all, 16)
        .border(.secondary)
        .onReceive(viewModel.objectWillChange, carry out: { _ in
            renderCount += 1
            print("(kind(of: self)) viewModel will change. depend: (renderCount)")
        })
        .sheet(isPresented: $viewModel.isSheetPresented) {
            let params = SheetViewModelParams(
                initialCount: 0,
                initialViewType: .Listing
            )
            
            /**
             - Bug: ViewModel won't 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 preliminary
    case loading
    case success
    case failure
}

ContentWithoutStateViewModelParams

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

ContentWithoutStateViewModel.swift

last class ContentWithoutStateViewModel: ObservableObject {
    @Printed var depend: Int
    @Printed var onSubmitStatus: OnSubmitStatus
    
    @Printed var isSheetPresented: Bool
    
    init(params: ContentWithoutStateViewModelParams) {
        self.depend = params.initialCount
        self.onSubmitStatus = params.initialOnSubmitStatus
        self.isSheetPresented = params.initialIsSheetPresented
        print("(kind(of: self)) (#perform)")
    }
    
    deinit {
        print("(kind(of: self)) (#perform)")
    }
    
    func fetchContent() async -> End result<Bool, Error> {
        sleep(1)
        return .success(true)
    }
    
    @MainActor
    func onSubmit() async {
        onSubmitStatus = .loading
        
        let end result = await fetchContent()
        
        end result.fold { success in
            depend += 1
            onSubmitStatus = .success
        } errorTransform: { failure in
            depend -= 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)
        )
    }
    
    @out there(
        *,
         deprecated,
         message: "Bug: ViewModel won't deinit when Sheet is dismissed. use .init(params:)")
    init(viewModel: SheetViewModel) {
        self._viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var physique: some View {
        VStack(spacing: 8) {
            Textual content("Sheet")
            
            CountComponent(
                depend: $viewModel.depend,
                onDecrement: {
                    viewModel.depend -= 1
                },
                onIncrement: {
                    viewModel.depend += 1
                }
            )
            
            Button {
                viewModel.selectedViewType = .Listing
            } label: {
                Textual content("present the sheet record")
            }
            
            Button {
                viewModel.selectedViewType = .New
            } label: {
                Textual content("present the sheet new")
            }
            
            change viewModel.selectedViewType {
            case .Listing:
                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, carry out: { _ in
            renderCount += 1
            print("(kind(of: self)) viewModel will change. depend: (renderCount)")
        })
    }
}

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

SheetViewType.swift

enum SheetViewType {
    case Listing
    case New
}

SheetViewModelParams.swift

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

SheetViewModel.swift

import Basis

last class SheetViewModel: ObservableObject {
    let id: UUID = UUID()
    
    @Printed var depend: Int
    @Printed var selectedViewType: SheetViewType
    
    init(
        params: SheetViewModelParams
    ) {
        self.depend = params.initialCount
        self.selectedViewType = params.initialViewType
        print("(kind(of: self)) (#perform) (id)")
    }
    
    deinit {
        print("(kind(of: self)) (#perform) (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!
    }
    
    non-public static weak var _shared: SheetViewModel?
}

=== Sheet Listing

struct SheetListView: View {
    @State var renderCount: Int = 0
    
    @StateObject var viewModel: SheetListViewModel
    
    init(params: SheetListViewModelParams) {
        self._viewModel = StateObject(
            wrappedValue: SheetListViewModel.shared(params: params)
        )
    }
    
    var physique: some View {
        VStack(spacing: 8) {
            Textual content("Sheet Listing")
            
            CountComponent(
                depend: $viewModel.depend,
                onDecrement: {
                    viewModel.depend -= 1
                },
                onIncrement: {
                    viewModel.depend += 1
                }
            )
        }
        .padding(.all, 16)
        .border(.secondary)
        .onReceive(viewModel.objectWillChange, carry out: { _ in
            renderCount += 1
            print("(kind(of: self)) viewModel will change. depend: (renderCount)")
        })
    }
}

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

SheetListViewModelParams.swift

import Basis

struct SheetListViewModelParams {
    let initialCount: Int
}

SheetListViewModel.swift

import Basis

last class SheetListViewModel: ObservableObject {
    let id = UUID()
    
    @Printed var depend: Int
    
    init(params: SheetListViewModelParams) {
        self.depend = params.initialCount
        
        print("(kind(of: self)) (#perform) (id)")
    }
    
    deinit {
        print("(kind(of: self)) (#perform) (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!
    }
    
    non-public static weak var _shared: SheetListViewModel?
}

=== Sheet New

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 physique: some View {
        VStack(spacing: 8) {
            Textual content("Sheet New")
            
            CountComponent(
                depend: $viewModel.depend,
                onDecrement: {
                    viewModel.depend -= 1
                },
                onIncrement: {
                    viewModel.depend += 1
                }
            )
        }
        .padding(.all, 16)
        .border(.secondary)
        .onReceive(viewModel.objectWillChange, carry out: { _ in
            renderCount += 1
            print("(kind(of: self)) viewModel will change. depend: (renderCount)")
        })
    }
}

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

SheetNewViewModelParams.swift

struct SheetNewViewModelParams {
    let initialCount: Int
}

SheetNewViewModel.swift

import Basis

last class SheetNewViewModel: ObservableObject {
    let id: UUID = UUID()
    
    @Printed var depend: Int
    
    init(params: SheetNewViewModelParams) {
        self.depend = params.initialCount
        print("(kind(of: self)) (#perform) (id)")
    }
    
    deinit {
        print("(kind(of: self)) (#perform) (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!
    }
    
    non-public static weak var _shared: SheetNewViewModel!
}

Latest news
Related news

LEAVE A REPLY

Please enter your comment!
Please enter your name here