Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory leak when initializing @ObservedObject inside View

Tags:

ios

swift

swiftui

I have a ViewModel class that is an ObservableObject and I initialize it when initializing its corresponding view. It seems that if I have any bindings to the ViewModel inside the view, the ViewModel is leaked.

For example, if I show said view inside a sheet, every time I present the sheet, a new reference is allocated and it does not get deallocated when the sheet is dismissed. The reference count keeps growing as many times I present the sheet.

Am I missing something, or is the @ObservedObject property wrapper not supposed to be used this way?

This is a simple example that exhibits the issue. The deinit function is never called for the ViewModel

struct NewContactView: View {

    class ViewModel: ObservableObject {

        @Published var firstName = ""
        @Published var lastName = ""
        @Published var email = ""
        @Published var phoneNumber = ""

        init() {
            print("INIT")
        }

        deinit {
            print("DEINIT")
        }

    }

    @ObservedObject private var viewModel = ViewModel()

    var didCreateNewContact: (Contact) -> Void = { _ in }

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Names")) {
                    TextField("First Name", text: $viewModel.firstName)
                    TextField("Last Name", text: $viewModel.lastName)
                }

                Section(header: Text("Details")) {
                    TextField("Email", text: $viewModel.email)
                    TextField("Phone Number", text: $viewModel.phoneNumber)
                }

                Button(action: {}) {
                    Text("Save")
                }
            }
        }
    }
}

Edit: Added code that presents the sheet

struct ContactsListView: View {

    @EnvironmentObject var contactStore: ContactStore

    @State private var isCreatingNewContact = false

    var body: some View {

            List(contactStore.contacts) { contact in
                ContactListItem(contact: contact)
            }
            .navigationBarItems(trailing: Button(action: { self.isCreatingNewContact = true }) {
                Image(systemName: "plus")
            })


            .sheet(isPresented: $isCreatingNewContact) {
                NewContactView(didCreateNewContact: self.createNewContactHandler)
        }
    }


    private func createNewContactHandler(_ contact: Contact) {
        contactStore.contacts.append(contact)
        isCreatingNewContact = false
    }

}

Edit 2: Memory Graph Screenshots

Memory graph

Edit 3: Weirdly enough, replacing the Form view with a VStack gets rid of the leak. If I use a List view instead of the VStack, the leak comes back.

like image 255
A.Andino Avatar asked Nov 07 '22 12:11

A.Andino


2 Answers

Use @StateObject so it will init only once, for iOS 14

Never do this: @ObservedObject private var viewModel = ViewModel(), it will create viewmodels all the time. You need to create this object at beginning of the app and pass it down where will not be recreated, or you can create in some view that is not updating as Dashboard depends on your app structure.

like image 162
zdravko zdravkin Avatar answered Nov 15 '22 13:11

zdravko zdravkin


It does not look there is a leak. Here is my setup of views (added titles to screen for easy reference - code is after the explanation).

Navigating from

User Setup -> Contact Related -> Add Contact

and going back all the way to main screen User Setup and then navigating to the next screen releases the previously created instance of the view model. Note: I added the unique id to the model which prints the id when the model is getting inited and deinited.

This leads me to believe that Models being observed are not deinited as soon as the view that is using it is getting released. Swift runtime controls when the observable model should be released. Also when I did the profiling for abandoned memory, I did not see any increase in the memory growth. Also when the previously instance of the view model is getting released, there is a dip in the memory which proves the point that it is getting deallocated at that moment.

struct ContentView: View {
    var body: some View {
        
        NavigationView {
        VStack {
             NavigationLink(destination: ContactRelated() ) {
                 Text("Contact Related")
             }
             Spacer()
        }
        .navigationBarTitle("User Setup")
        }
    }
}
struct ContactRelated: View {
        
    var body: some View {
    
        VStack {
             NavigationLink(destination: NewContactView() ) {
                 Text("Add New Contact")
             }
             Spacer()
        }
        .navigationBarTitle("Contact")
    }
}
struct NewContactView: View {
    
    class ViewModel: ObservableObject {
        
        var id = UUID()
        
        @Published var firstName = ""
        @Published var lastName = ""
        @Published var email = ""
        @Published var phoneNumber = ""
        
        init() {
            print(">>>init \(id)")
        }
        
        deinit {
            print(">>>deinit \(id)")
        }
        
    }
    
    @ObservedObject private var viewModel = ViewModel()
    
    //var didCreateNewContact: (Contact) -> Void = { _ in }
    
    var body: some View {
        
        Form {
            
            Section(header: Text("Names")) {
                TextField("First Name", text: $viewModel.firstName)
                TextField("Last Name", text: $viewModel.lastName)
            }
            
            Section(header: Text("Details")) {
                TextField("Email", text: $viewModel.email)
                TextField("Phone Number", text: $viewModel.phoneNumber)
            }
            
            Button(action: {}) {
                Text("Save")
            }
        }
    .navigationBarTitle("Add Contact")
        
    }
}

Generally I will not add the definition of the observable view model within the struct.

like image 29
Partha G Avatar answered Nov 15 '22 12:11

Partha G