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:
- Navigate to
SecondView
- Navigate to
ThirdView
- Do not focus on either
field 1
orfield 2
- Go back to
SecondView
- Go back to
FirstView
DEINIT
is printed to the debug console
Steps that do reproduce the issue:
- Navigate to
SecondView
- Navigate to
ThirdView
- Focus on either
field 1
orfield 2
- Go back to
SecondView
- Go back to
FirstView
DEINIT
is not printed to the debug console
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.