@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.

Post not yet marked as solved Up vote post of ulian_onua Down vote post of ulian_onua
2.0k views

Replies

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

  • I don't have any answers here, but thanks for filing FB13195534.

  • Is the feedback private? If I click on it it says not found.

  • @DusanMaster guess so.

Add a Comment

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)
        }
    }
}
Post not yet marked as solved Up vote reply of malc Down vote reply of malc
  • Unfortunately, if you run this code on iOS 17 and present sheet several times, you can see with Debug Memory Graph that leaks happen (for example, you open sheet 4 times – there are 4 Swift closure context leaks). And this issue doesn't happen on iOS 16, so the root of problem must lie somewhere deeper.

  • @ malc This is absolutely incorrect advice. I recommend you learn the basics before giving advice to others. This example for instance gives you a good overview on "Correct SwiftUI code" as well as proper use of view models.

Add a Comment

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

Add a Comment