An Observable class held by a SwiftUI View is not deinitialized

I'm using the new Observation framework to separate the state of a SwiftUI View into an observable class, which the view holds onto.

However, in some cases, even after the view's lifecycle ends, the observable class is not deallocated.

Environment

  • Xcode 16.2 (16C5032a)
  • Deployment Target: iOS 17.2
  • iPhone SE (3rd generation) iOS 18.1 Simulator

Reproduction Code

import SwiftUI
import Observation

struct FirstView: View {
  var body: some View {
    NavigationStack {
      List {
        NavigationLink("SecondView") {
          SecondView()
        }
      }
    }
  }
}

struct SecondView: View {
  var body: some View {
    NavigationStack {
      List {
        NavigationLink("ThirdView") {
          ThirdView()
        }
      }
    }
  }
}

struct ThirdView: View {
  @Bindable private var state = ThirdViewState()
  @FocusState private var focusedField: Field?
  
  private enum Field: Hashable {
    case field1, field2
  }
  
  var body: some View {
    Form {
      TextField("field 1", text: $state.field1)
        .focused($focusedField, equals: .field1)
        .onSubmit {
          focusedField = .field2
        }
      TextField("field 2", text: $state.field2)
        .focused($focusedField, equals: .field2)
    }
  }
}

@Observable final class ThirdViewState {
  var field1: String = ""
  var field2: String = ""
  
  deinit {
    print("DEINIT")
  }
}

Steps to Reproduce

Steps that do not reproduce the issue:

  1. Navigate to SecondView
  2. Navigate to ThirdView
  3. Do not focus on either field 1 or field 2
  4. Go back to SecondView
  5. Go back to FirstView
  6. DEINIT is printed to the debug console :white_check_mark:

Steps that do reproduce the issue:

  1. Navigate to SecondView
  2. Navigate to ThirdView
  3. Focus on either field 1 or field 2
  4. Go back to SecondView
  5. Go back to FirstView
  6. DEINIT is not printed to the debug console :cross_mark:

When I looked at the memory graph during reproduction, I noticed a reference to .onSubmit.
If I comment out the closure inside the .onSubmit modifier, the ThirdViewState is properly deallocated.
However, doing so prevents me from changing focus to another TextField when the submit event occurs — which is the behavior I need.

Additionally, I’ve confirmed an odd behavior:
After completing steps 1-6 (reproduce the issue), repeating steps 1-3 will output DEINIT in step 3 and release the ThirdViewState instance that had not been previously released.


Is there something I'm doing wrong?
If anyone has insight into this behavior, I would greatly appreciate your help.

1 Like

It seems that a similar issue was reported on Apple's Developer Forums.
As mentioned in this post, it was confirmed that using the deprecated TextField initializer with the onCommit argument can work as a workaround.

I have prepared an even more simplified reproduction code.
Just referencing a View's property inside the onSubmit closure causes the model class not to be deallocated.

import SwiftUI
import Observation

struct FirstView: View {
  @State private var isPresented: Bool = false
  
  var body: some View {
    Button("show") {
      isPresented = true
    }
    .sheet(isPresented: $isPresented) {
      SecondView()
    }
  }
}

struct SecondView: View {
  private var model = Model()
  private let a: Bool = true
  
  var body: some View {
    TextField("field", text: .constant(""))
      .onSubmit { _ = a }
  }
}

@Observable final class Model {
  deinit {
    print("DEINIT")
  }
}

Also, since the issue can be reproduced not only with the Observation framework but also with ObservableObject, it may not be a problem specific to the Observation framework, but rather an issue with the onSubmit modifier itself.