When presenting a SwiftUI sheet containing ObservableObject's injected using environmentObject(_)
modifier, the objects are unexpectedly retained after the sheet is dismissed if a TextField within the sheet gains focus or is edited.
This issue occurs on iOS and iPadOS (on macOS the objects are always released), observable both in the simulator and on physical devices, and happens even when the view does not explicitly reference these environment objects, and the TextField's content isn't bound to them.
Expected Results:
When the sheet is dismissed, all environment objects passed to the sheet’s content view should be released (deinitialized), regardless of whether the TextField was focused or edited.
Actual Results:
- If the TextField was focused or edited, environment objects (ObservableA and ObservableB) are retained after the sheet is dismissed. They are not deinitialized as expected, leading to unintended retention.
- Interestingly, previously retained copies of these environment objects, if any, are released precisely at the moment the TextField becomes focused on subsequent presentations, indicating an inconsistent lifecycle behavior.
I have filed an issue FB17226970
Sample Code
Below is a sample code that consistently shows the issue on iOS 18.3+.
Steps to Reproduce:
- Run the attached SwiftUI sample.
- Tap the button labeled “Show Sheet” to present a sheet.
- Tap on the TextField to focus or begin editing.
- Dismiss the sheet by dragging it down or by other dismissal methods (e.g., tapping outside on iPadOS).
import SwiftUI struct ContentView: View { @State private var showSheet: Bool = false var body: some View { VStack { Button("Show Sheet") { showSheet = true } } .sheet(isPresented: $showSheet) { SheetContentView() .environmentObject(ObservableA()) .environmentObject(ObservableB()) } } } struct SheetContentView: View { @State private var text: String = "" var body: some View { TextField("Select to retain observable objects", text: $text) .textFieldStyle(.roundedBorder) } } final class ObservableA: ObservableObject { init() { print(type(of: self), #function) } deinit { print(type(of: self), #function) } } final class ObservableB: ObservableObject { init() { print(type(of: self), #function) } deinit { print(type(of: self), #function) } } #Preview { ContentView() }