Damit ein SwiftUI-Sheet während Aktualisierungen geöffnet bleibt, muss die Identität seines Präsentationshosts erhalten bleiben und seine Präsentationsbindung aktiv bleiben. Füge das Sheet an einen stabilen Container an, damit sich ändernde untergeordnete Inhalte aktualisiert werden können, ohne den Präsentator zu ersetzen.
Diese Anleitung richtet sich an Entwickler, die eine Mac-App mit einem bearbeitbaren Sheet erstellen, das sichtbar bleiben soll, während sich die zugrunde liegende Oberfläche ändert. Das ausgearbeitete Beispiel verwendet synthetischen lokalen Text und trennt drei Dinge, die du unabhängig voneinander untersuchen kannst: den Präsentationsstatus, die Lebensdauer der präsentierenden Ansicht und den Inhalt des Editors.
Warum kann eine Ansichtsaktualisierung ein Sheet verschwinden lassen?
SwiftUI kann bei der Auswertung von body neue Ansichtsinstanzen erzeugen und dabei die Identität der angezeigten Ansicht beibehalten. Der Zustand bleibt während der Lebensdauer dieser Identität erhalten. Wird die Ansicht entfernt oder ihre Identität geändert, endet diese Lebensdauer; auch der Wechsel zwischen bedingten Zweigen kann unterschiedliche Identitäten erzeugen, selbst wenn beide Zweige denselben Ansichtstyp enthalten. Apple erläutert diese Zusammenhänge in Demystify SwiftUI.
Bei einem Bool-gesteuerten Sheet ist die dokumentierte Präsentationssteuerung die an sheet(isPresented:onDismiss:content:) übergebene Bindung: Ein Wert von true fordert die Präsentation an. Wenn du untersuchst, warum ein Sheet verschwindet, prüfe sowohl diese Bindung als auch, ob die Ansicht, die den Modifier trägt, weiterhin besteht. Ein an anderer Stelle gespeicherter true-Wert belegt nicht, dass der ursprüngliche Präsentationshost erhalten geblieben ist. Siehe Apples Dokumentation zu Boolean-Sheets.
Eine besonders direkte Ursache ist eine sich ändernde explizite Kennung. Apple dokumentiert, dass eine Änderung des an id(_:) übergebenen Werts die Identität der Ansicht und damit auch ihren Zustand zurücksetzt. Prüfe die präsentierende Ansicht und ihre Vorfahren auf Kennungen, die aus Revisionszählern, bearbeitbarem Text oder neu erzeugten Werten abgeleitet werden. Entferne solche Kennungen, wenn ihr Zurücksetzverhalten unbeabsichtigt ist. Siehe id(_:).
Die praktische Folgerung für das Design ist, den Sheet-Modifier außerhalb des sich ändernden Teils der Oberfläche zu platzieren. Wenn eine Ladeansicht, eine gefilterte Zeile oder ein alternatives Detail-Layout verschwinden kann, ist der nächste geeignete dauerhafte Vorfahr in der Regel ein besserer Präsentationsbesitzer.
Einen stabilen Sheet-Besitzer mit einem vollständigen Mac-Beispiel erstellen
Erstelle in Xcode eine macOS-SwiftUI-App, lege das Deployment-Ziel auf macOS 12 oder neuer fest und ersetze die generierten Deklarationen für App und Content-View durch die folgende einzelne Datei. Behalte nur eine @main-App-Deklaration. Die Mindestversion macOS 12 ermöglicht die im Beispiel verwendete dismiss-Umgebungsaktion; TextEditor ist bereits ab macOS 11 verfügbar. Siehe Apples Dokumentation zu dismiss und TextEditor.
Die Root-Ansicht des Fensters verwaltet das Präsentationsflag, den bearbeitbaren Text, den Revisionszähler und das Layout-Flag. Ihr äußerer VStack trägt den Sheet-Modifier. Nur der Inhalt innerhalb dieses Containers wechselt zwischen Zweigen. Apple empfiehlt, den Zustand privat in der höchsten Ansicht zu deklarieren, die ihn benötigt, und Bindungen an untergeordnete Ansichten weiterzugeben, die ihn ändern müssen. Siehe 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)
}
}
Dies ist eine vollständige Demo-App, die anhand der dokumentierten APIs geprüft wurde. Sie wurde hier nicht kompiliert oder ausgeführt, da diese Umgebung weder über eine Swift-Toolchain noch eine macOS-Laufzeitumgebung verfügt.
Die Aktualisierung ausführen, ohne das Sheet zu schließen
- Starte die App und klicke auf
Edit Note. - Ersetze den Beispieltext durch
Sample note Beta. - Klicke mehrmals auf
Update Owner. Jeder Klick erhöht den Zustand des Besitzers und wechselt den bedingten Inhalt des Besitzers. - Vergewissere dich, dass der Editor sichtbar bleibt, sein Text weiterhin
Sample note Betalautet und seine Revisionsanzeige fortgeschrieben wird. - Klicke auf
Done. Vergewissere dich, dass das Fenster den bearbeiteten Text im erweiterten Layout anzeigt, wenn dieses Layout aktiv ist. - Öffne den Editor erneut und vergewissere dich, dass der bearbeitete Text weiterhin verfügbar ist.
Die Aktualisierungsschaltfläche befindet sich im Sheet, sodass der Test nicht davon abhängt, in das darunterliegende Fenster zu klicken. AppKit dokumentiert Sheets als dokumentmodal: Solange ein Sheet sichtbar ist, sind die meisten an sein Elternfenster gerichteten Ereignisse nicht zulässig. Die Aktualisierung des Besitzers über eine Bindung ermöglicht es diesem Beispiel, die Zustandsänderung direkt auszuführen. Siehe beginSheet(_:completionHandler:).
Festlegen, was das Schließen des Editors bedeuten soll
Dieses Beispiel bearbeitet den Text des Besitzers unmittelbar. Done schließt den Editor; es führt keinen separaten Speichervorgang aus. Die Zeichenfolge bleibt im fensterlokalen Zustand erhalten, und das Beispiel bietet keine Persistenz über App-Starts hinweg. Dieses Verhalten entspricht dem Design mit gemeinsam genutzter Bindung, das in State beschrieben wird.
Wenn deine Aufgabe einen abbrechbaren Entwurf erfordert, verwende einen separaten Entwurfswert und kopiere ihn nur nach einer ausdrücklichen Bestätigungsaktion in das Dokument. Lege dieses Verhalten fest, bevor du das Schließen implementierst: Ob ein Entwurf übernommen oder verworfen werden soll, ist eine andere Frage als die, welche Ansicht das Sheet besitzt.
Das Schließen mit drei unabhängigen Prüfungen untersuchen
Gehe in deiner eigenen App wie folgt vor. So werden die allgemeinen Identitätsregeln zu einer gezielten Untersuchung, ohne anzunehmen, dass jedes verschwindende Sheet dieselbe Ursache hat.
| Prüfung | Zu untersuchen | Praktische Korrektur |
|---|---|---|
| Präsentationswert | Jede Zuweisung, die den Boolean auf false setzt oder das optionale Präsentationselement leert |
Behalte Schließzuweisungen beabsichtigten Aktionen zum Schließen vor |
| Identität des Präsentators | Bedingte Zweige, entfernte Zeilen, sich ändernde Kennungen von Vorfahren | Verschiebe sowohl den Präsentationszustand als auch den Modifier auf einen geeigneten stabilen Vorfahren |
| Lebensdauer des Inhalts | Lokaler Sheet-Zustand, sich ändernde Inhaltskennungen, Synchronisierung des Editors | Erhalte die beabsichtigte Bearbeitungssitzung und aktualisiere vorhandene Inhalte gezielt |
Suche zunächst in deinem Projekt nach Zuweisungen an die Präsentationseigenschaft und nach Aufrufen von dismiss(). Ein Aktualisierungs-Callback könnte das Sheet ausdrücklich schließen. Lies dismiss in der eigenen Ansicht des Sheets aus, wie im Beispiel: Apple dokumentiert, dass die Aktion für die Umgebung gilt, in der sie abgerufen wird. Wenn du sie in der Root-Ansicht des Fensters ausliest, kann sie sich stattdessen auf dieses Fenster beziehen. Siehe dismiss.
Untersuche als Nächstes die Hierarchie oberhalb des Modifiers. Wenn das Sheet an eine Detailansicht angefügt ist, die bei einer Aktualisierung entfernt wird, verschiebe es oberhalb dieser Ersetzungsgrenze. Verschiebe auch den lokal verwalteten Präsentationszustand dorthin. Bleibt das Flag in einem untergeordneten Element, dessen Identität endet, ist dessen Zustandsspeicher laut Apples Erläuterung zur Lebensdauer von Ansichten anfällig für eine Ersetzung.
Unterscheide drittens zwischen einer geschlossenen Präsentation und einem Editor, der sichtbar bleibt, aber seinen Entwurf verliert. Beginne mit dem einfachen TextEditor-Beispiel und füge dann deine eigenen Inhalte schrittweise wieder hinzu. Dieser Vergleich ist eine Diagnosemethode: Er hilft festzustellen, ob der Fehler mit der Zuständigkeit für die Präsentation oder mit deiner Logik zur Inhaltsaktualisierung zusammenhängt.
Solltest du zu einem elementgesteuerten Sheet wechseln?
Verwende sheet(item:onDismiss:content:), wenn ein bestimmtes ausgewähltes Objekt die Quelle der Präsentation ist. Apple dokumentiert ein optionales Element, das Identifiable entspricht, übergibt der Inhalts-Closure ein nicht-nil-Element und beschreibt den Austausch, wenn sich das Element ändert. Ein Wechsel der Überladung erhält nicht automatisch einen Präsentator, dessen Lebensdauer endet. Siehe elementgesteuerte Sheets.
Wähle eine Kennung, die das Objekt während der vorgesehenen Bearbeitungssitzung repräsentiert. Das Swift-Protokoll Identifiable drückt eine stabile Identität aus; seine Dokumentation überlässt den teilnehmenden Typen ausdrücklich die erforderliche Dauer und den Geltungsbereich. Eine bei jedem Lesen einer Eigenschaft neu erzeugte Kennung kann kein fortbestehendes Objekt repräsentieren. Siehe Identifiable.
Für einen einfachen Editor wie in der Demo macht ein boolesches Präsentationsflag zusammen mit separaten Textbindungen den beabsichtigten Zusammenhang leicht nachvollziehbar. Bei einem Editor für ein ausgewähltes Objekt solltest du die Identität der Auswahl von häufig wechselnden Feldern getrennt halten. Behandle die Auswahl eines anderen Objekts als bewusste Präsentationsentscheidung und lege zuerst fest, was mit dem aktuellen Entwurf geschehen soll.
Bleibt das Sheet geöffnet, wenn die interaktive Schließung deaktiviert wird?
interactiveDismissDisabled(_:) steuert nicht programmatische Schließvorgänge. Apple erklärt ausdrücklich, dass dadurch weder das Schließen aufgrund einer Änderung der Präsentationsbindung noch durch den Aufruf von dismiss verhindert wird. Die dokumentierte Funktion bietet daher keine Abhilfe bei einer Ersetzung der Identität. Siehe interactiveDismissDisabled(_:).
Wende den Modifier innerhalb der Sheet-Hierarchie nur an, wenn deine Oberfläche dieses Schließverhalten benötigt, und behalte einen ausdrücklichen Weg zum Abschließen bei. Beginne für diese Aufgabe damit, den Host und seinen Zustand zu erhalten. Wenn du eine Einschränkung für das Schließen hinzufügst, bevor du die Ursache ermittelt hast, bleibt das ursprüngliche Problem mit der Zuständigkeit möglicherweise ungelöst.
Was ändert sich, wenn das Sheet AppKit- oder TextKit-Inhalte enthält?
Trenne bei einem mit NSViewRepresentable eingebetteten NSTextView das Erstellen vom Aktualisieren. Apple definiert makeNSView(context:) als Erstellungsschritt und updateNSView(_:context:) als den Ort, an dem nachfolgende SwiftUI-Informationen angewendet werden. Ein Koordinator leitet Änderungen über Delegate- oder Target-Action-Mechanismen an SwiftUI zurück. Siehe NSViewRepresentable und makeNSView(context:).
Eine aus diesem Lebenszyklus abgeleitete nützliche Implementierungsregel lautet, den Editor nicht allein deshalb neu zu erstellen, weil sich ein nicht damit zusammenhängender Zustand des Besitzers geändert hat. Bevor du seinen Inhalt ersetzt, ermittle, ob sich der eingehende Modelltext tatsächlich unterscheidet und wie mit gleichzeitig vorgenommenen lokalen Änderungen umzugehen ist. Ein stabiles Sheet löst für sich genommen keine konkurrierenden Schreibvorgänge auf seinen Text.
TextKit stellt Textspeicherung, Layout und Darstellung bereit. AppKits NSTextView bündelt diese Funktionen und stellt moderne sowie ältere Layout-Engines bereit. Diese Mechanismen betreffen den Inhalt des Editors; die Zuständigkeit für die Präsentation benötigt weiterhin eine eigene stabile Lebensdauer. Verwende TextKit, wenn die Bearbeitungsaufgabe es erfordert, und untersuche das Verhalten bei Textaktualisierungen getrennt vom Schließen des Sheets. Siehe Apples TextKit-Überblick.
Wann ist eine Präsentation durch ein AppKit-Fenster sinnvoll?
Wenn deine bestehende App ihren Fensterlebenszyklus bereits in AppKit verwaltet, führt macOS 26 NSWindow.beginSheet(content:completionHandler:) ein, um SwiftUI-Inhalte direkt in diesem Fenster zu präsentieren. Apple dokumentiert die Unterstützung für die Umgebungsaktion zum Schließen des Sheets, Größenanpassung, Symbolleisten und die Steuerung der interaktiven Schließung. Verwende ein macOS-26-SDK und beachte die Verfügbarkeitsgrenze von macOS 26, bevor du es einsetzt. Siehe beginSheet(content:completionHandler:).
Mit dem zurückgegebenen NSWindow.HostingSheetRepresentation lässt sich die präsentierte Root-Ansicht ändern oder das Schließen programmgesteuert über AppKit auslösen. Es bietet eine dokumentierte Option, wenn eine Zuständigkeit auf Fensterebene zur App-Architektur passt. Für das obige Beispiel mit einem stabilen SwiftUI-Container ist es nicht erforderlich. Siehe NSWindow.HostingSheetRepresentation.
Für frühere macOS-Versionen akzeptiert AppKits bestehendes beginSheet(_:completionHandler:) ein Sheet vom Typ NSWindow und dokumentiert das Einreihen von Präsentationen. Gehe nicht davon aus, dass die neuere Überladung für SwiftUI-Inhalte auf diesen Systemen verfügbar ist. Siehe beginSheet(_:completionHandler:).
Behalte einen stabilen Besitzer, einen aktiven Präsentationswert und die vorgesehene Quelle der Wahrheit für den Editor bei. Überprüfe diese Bedingungen mit einer kleinen lokalen Aktualisierungsaktion, bevor du komplexere Inhalte wieder hinzufügst. So erhältst du eine konkrete Möglichkeit, die Bearbeitung während einer Aktualisierung der Oberfläche fortzusetzen, innerhalb der von Apple dokumentierten Lebensdauer- und API-Grenzen.
Quellen
- 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