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
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With