To keep a SwiftUI sheet open during updates, preserve its presentation host’s identity and keep its presentation binding active. Attach the sheet to a stable container so changing child content can update without replacing the presenter.
This guide is for a developer building a Mac app with an editable sheet that should remain visible while the underlying interface changes. The worked example uses synthetic local text and separates three things you can inspect independently: presentation state, the presenting view’s lifetime, and the editor’s contents.
Why can a view update make a sheet disappear?
SwiftUI can create new view values when evaluating body while preserving the identity of the displayed view. State persists for that identity’s lifetime. Removing the view or changing its identity ends that lifetime; switching between conditional branches can also create distinct identities, even when both branches contain the same view type. Apple explains these relationships in Demystify SwiftUI.
For a Boolean-driven sheet, the documented presentation control is the binding passed to sheet(isPresented:onDismiss:content:): a true value requests presentation. When investigating a disappearing sheet, check both that binding and the continued existence of the view carrying the modifier. A true value stored elsewhere does not establish that the original presentation host survived. See Apple’s Boolean sheet documentation.
A particularly direct cause is a changing explicit identifier. Apple documents that changing the value supplied to id(_:) resets the view’s identity, including its state. Inspect the presenting view and its ancestors for identifiers derived from revision counters, editable text, or newly generated values. Remove such identifiers when their reset behavior is unintended. See id(_:).
The practical design inference is to place the sheet modifier outside the changing portion of the interface. If a loading view, a filtered row, or an alternate detail layout can disappear, its nearest suitable persistent ancestor is usually a better presentation owner.
Build a stable sheet owner with a complete Mac example
Create a macOS SwiftUI app in Xcode, set its deployment target to macOS 12 or later, and replace the generated app and content-view declarations with the following single file. Keep only one @main app declaration. The macOS 12 baseline accommodates the example’s dismiss environment action; TextEditor itself is available from macOS 11. See Apple’s dismiss documentation and TextEditor documentation.
The window’s root view owns the presentation flag, editable text, revision counter, and layout flag. Its outer VStack carries the sheet modifier. Only the content inside that container changes branches. Apple recommends declaring state privately at the highest view that needs it and passing bindings to children that need to change it. See State.
import SwiftUI
@main
struct StableSheetDemoApp: App {
var body: some Scene {
WindowGroup {
SheetOwnerView()
}
}
}
struct SheetOwnerView: View {
@State private var isEditorPresented = false
@State private var note = "Sample note Alpha"
@State private var revision = 0
@State private var compactLayout = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Local Notes")
.font(.title)
if compactLayout {
Text("Compact summary: revision \(revision)")
} else {
VStack(alignment: .leading, spacing: 8) {
Text("Expanded summary")
Text("Revision \(revision)")
Text(note)
}
}
Button("Edit Note") {
isEditorPresented = true
}
}
.padding(24)
.frame(minWidth: 440, minHeight: 260)
.sheet(isPresented: $isEditorPresented) {
NoteEditorSheet(
note: $note,
revision: $revision,
compactLayout: $compactLayout
)
}
}
}
struct NoteEditorSheet: View {
@Binding var note: String
@Binding var revision: Int
@Binding var compactLayout: Bool
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Edit Sample Note")
.font(.headline)
TextEditor(text: $note)
.frame(minHeight: 160)
Text("Owner revision: \(revision)")
Button("Update Owner") {
revision += 1
compactLayout.toggle()
}
HStack {
Spacer()
Button("Done") {
dismiss()
}
}
}
.padding(24)
.frame(width: 460, height: 340)
}
}
This is a complete demonstration app, inspected against the documented APIs. It has not been compiled or run here because this environment has no Swift toolchain or macOS runtime.
How to exercise the update without closing the sheet
- Run the app and click
Edit Note. - Replace the sample text with
Sample note Beta. - Click
Update Ownerseveral times. Each click increments owner state and switches the owner’s conditional content. - Confirm that the editor remains visible, its text remains
Sample note Beta, and its revision display advances. - Click
Done. Confirm that the window displays the edited text in the expanded layout when that layout is active. - Reopen the editor and confirm that the edited text remains available.
The update button sits inside the sheet so the test does not depend on clicking the underlying window. AppKit documents sheets as document-modal: while a sheet is visible, most events directed at its parent window are prohibited. Updating the owner through a binding lets this example exercise the state change directly. See beginSheet(_:completionHandler:).
Decide what closing the editor should mean
This example edits the owner’s text immediately. Done closes the editor; it does not perform a separate save operation. The string remains in window-local state, and the example provides no persistence across app launches. That behavior follows the shared binding design described in State.
If your task requires a cancelable draft, use a separate draft value and copy it to the document only on an explicit confirmation action. Define that behavior before implementing dismissal: deciding whether to commit or discard a draft is separate from deciding which view owns the sheet.
Diagnose the dismissal with three independent checks
Use the following sequence on your own app. It turns the general identity rules into a focused investigation, without assuming that every disappearing sheet has the same cause.
| Check | What to inspect | Practical correction |
|---|---|---|
| Presentation value | Every write that sets the Boolean to false or clears the optional presentation item |
Keep dismissal writes in intentional close actions |
| Presenter identity | Conditional branches, removed rows, changing ancestor identifiers | Move both presentation state and the modifier to a suitable stable ancestor |
| Content lifetime | Sheet-local state, changing content identifiers, editor synchronization | Preserve the intended editing session and update existing content deliberately |
First, search your project for assignments to the presentation property and calls to dismiss(). A refresh callback may be explicitly closing the sheet. Read dismiss inside the sheet’s own view, as the example does: Apple documents that the action applies to the environment where it is obtained. Reading it in the window’s root view can target that window instead. See dismiss.
Second, inspect the hierarchy above the modifier. If the sheet is attached to a detail view that a refresh removes, move it above that replacement boundary. Move its locally owned presentation state as well. Keeping the flag in a child whose identity ends leaves the storage vulnerable to replacement, according to Apple’s view lifetime explanation.
Third, distinguish a closed presentation from an editor that remains visible but loses its draft. Start with the plain TextEditor example, then add your custom content back in stages. This comparison is a diagnostic method: it helps determine whether the failure follows presentation ownership or your content-update logic.
Should you switch to an item-driven sheet?
Use sheet(item:onDismiss:content:) when a particular selected entity is the source for the presentation. Apple documents an optional item conforming to Identifiable, passes a non-nil item into the content closure, and describes replacement when the item changes. Switching overloads does not by itself preserve a presenter whose lifetime ends. See item-driven sheets.
Choose an identifier that represents the entity throughout the intended editing session. Swift’s Identifiable protocol expresses stable identity, and its documentation explicitly leaves the required duration and scope to the participating types. A fresh identifier every time a property is read cannot represent a continuing entity. See Identifiable.
For a simple editor like the demonstration, a Boolean presentation flag plus separate text bindings makes the intended relationship easy to inspect. For a selected-item editor, keep selection identity separate from frequently changing fields. Treat selecting another entity as a deliberate presentation decision, and define what should happen to the current draft first.
Will disabling interactive dismissal keep the sheet alive?
interactiveDismissDisabled(_:) controls nonprogrammatic dismissal. Apple explicitly states that it does not prevent dismissal caused by changing the presentation binding or calling dismiss. Its documented role therefore does not provide a remedy for identity replacement. See interactiveDismissDisabled(_:).
Apply the modifier inside the sheet hierarchy only when your interface needs that dismissal behavior, and retain an explicit completion path. For this task, begin by preserving the host and its state. Adding a dismissal restriction before identifying the cause can leave the original ownership problem unresolved.
What changes when the sheet contains AppKit or TextKit content?
For an NSTextView embedded with NSViewRepresentable, separate creation from updates. Apple defines makeNSView(context:) as the creation step and updateNSView(_:context:) as the place to apply subsequent SwiftUI information. A coordinator carries delegate or target-action changes back into SwiftUI. See NSViewRepresentable and makeNSView(context:).
A useful implementation rule derived from that lifecycle is to avoid rebuilding the editor merely because unrelated owner state changed. Before replacing its contents, determine whether the incoming model text actually differs and how concurrent local edits should be handled. A stable sheet does not, by itself, resolve competing writes to its text.
TextKit supplies text storage, layout, and rendering. AppKit’s NSTextView packages those capabilities and exposes modern and legacy layout engines. That machinery concerns the editor’s content; presentation ownership still needs its own stable lifetime. Use TextKit when the editing task requires it, and investigate text-update behavior separately from sheet dismissal. See Apple’s TextKit overview.
When is window-owned AppKit presentation appropriate?
If your existing app already owns its window lifecycle in AppKit, macOS 26 introduces NSWindow.beginSheet(content:completionHandler:) for presenting SwiftUI content directly on that window. Apple documents support for the sheet’s dismiss environment action, sizing, toolbars, and interactive-dismissal control. Use a macOS 26 SDK and enforce the macOS 26 availability boundary before adopting it. See beginSheet(content:completionHandler:).
The returned NSWindow.HostingSheetRepresentation can change the presented root view or support programmatic dismissal from AppKit. It offers a documented option when window-level ownership fits the app’s architecture. It is unnecessary for the stable SwiftUI container example above. See NSWindow.HostingSheetRepresentation.
For earlier macOS versions, AppKit’s existing beginSheet(_:completionHandler:) accepts an NSWindow sheet and documents presentation queuing. Do not assume that the newer SwiftUI-content overload is available on those systems. See beginSheet(_:completionHandler:).
Keep the owner stable, keep the presentation value active, and preserve the editor’s intended source of truth. Verify those conditions with a small local update action before restoring more complex content. This gives you a concrete way to keep editing while the interface updates, within the lifetime and API boundaries Apple documents.
Sources
- Demystify SwiftUI — Apple
- sheet(isPresented:onDismiss:content:) — Apple
- id(_:) — Apple
- State — Apple
- dismiss — Apple
- TextEditor — Apple
- sheet(item:onDismiss:content:) — Apple
- Identifiable — Apple
- interactiveDismissDisabled(_:) — Apple
- NSViewRepresentable — Apple
- makeNSView(context:) — Apple
- TextKit — Apple
- beginSheet(content:completionHandler:) — Apple
- NSWindow.HostingSheetRepresentation — Apple
- beginSheet(_:completionHandler:) — Apple