@State ViewModel memory leak in iOS 17 (new Observable)

Our app has an architecture based on ViewModels.

Currently, we are working on migrating from the ObservableObject protocol to the Observable macro (iOS 17+). The official docs about this are available here: https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro

Our ViewModels that were previously annotated with @StateObject now use just @State, as recommended in the official docs.
Some of our screens (a screen is a SwiftUI view with a corresponding ViewModel) are presented modally. We expect that after dismissing a SwiftUI view that was presented modally, its corresponding ViewModel, which is owned by this view (via the @State modifier), will be deinitialized. However, it seems there is a memory leak, as the ViewModel is not deinitialized after a modal view is dismissed.

Here's a simple code where ModalView is presented modally (through the .sheet modifier), and ModalViewModel, which is a @State of ModalView, is never deinitialized.

import SwiftUI
import Observation

@Observable
final class ModalViewModel {
    init() {
        print("Simple ViewModel Inited")
    }
    
    deinit {
        print("Simple ViewModel Deinited") // never called
    }
}

struct ModalView: View {
    @State var viewModel: ModalViewModel = ModalViewModel()
    
    let closeButtonClosure: () -> Void
    
    var body: some View {
        ZStack {
            Color.yellow
                .ignoresSafeArea()
            Button("Close") {
                closeButtonClosure()
            }
        }
    }
}

struct ContentView: View {
    @State var presentSheet: Bool = false
    
    var body: some View {
        Button("Present sheet modally") {
            self.presentSheet = true
        }
        .sheet(isPresented: $presentSheet) {
            ModalView {
                self.presentSheet = false
            }
        }
    }
}

#Preview {
    ContentView()
}

Is this a bug in the iOS 17 beta version or intended behavior? Is it possible to build a relationship between the View and ViewModel in a way where the ViewModel will be deinitialized after the View is dismissed?

Thank you in advance for the help.

Answered by defagos in 769956022

As written on a related thread, the issue has been fixed in iOS 17.2 beta 1.

We are seeing the same issue, posted some feedback but no response yet. FB13195534

The solution is don't use view model objects! You have to learn the View struct which is designed to hold your view data in a memory efficient hierarchy. It has a lot of magical features like dependency tracking and change detection which you just have to learn to use SwiftUI effectively.

@Observable is for model data it won't work for view data. You might get close to implementing the same behaviour as the View struct but you'll eventually get stuck so it is a complete waste of time.

Correct SwiftUI code would look like this:


struct ModalConfig {
    var isPresented = false
    var otherVar = ""

     mutating func present() {
         isPresented = true
         otherVar = ""
      }
    
    mutating func dismiss() {
         isPresented = false
    }
}

struct ModalView: View {
    @Binding var config: ModalConfig
    
    var body: some View {
        ZStack {
            Color.yellow
                .ignoresSafeArea()
            Button("Close") {
                  config.dismiss()
            }
        }
    }
}

struct ContentView: View {
    @State var config = ModalConfig()
    
    var body: some View {
        Button("Present sheet modally") {
            config.present()
        }
        .sheet(isPresented: $config.isPresented) {
            ModalView(config: $config)
        }
    }
}

As written on a related thread, the issue has been fixed in iOS 17.2 beta 1.

@State ViewModel memory leak in iOS 17 (new Observable)
 
 
Q