Pour garder une feuille SwiftUI ouverte pendant les mises à jour, préservez l’identité de son hôte de présentation et maintenez active la liaison de présentation. Attachez la feuille à un conteneur stable afin que le contenu enfant puisse changer sans remplacer le présentateur.

Ce guide s’adresse aux développeurs qui créent une app Mac avec une feuille modifiable devant rester visible pendant que l’interface sous-jacente change. L’exemple utilise un texte local fictif et distingue trois éléments que vous pouvez examiner indépendamment : l’état de présentation, la durée de vie de la vue présentatrice et le contenu de l’éditeur.

Pourquoi une mise à jour de vue peut-elle faire disparaître une feuille ?

SwiftUI peut créer de nouvelles valeurs de vue lors de l’évaluation de body tout en préservant l’identité de la vue affichée. L’état persiste pendant toute la durée de vie de cette identité. La suppression de la vue ou la modification de son identité met fin à cette durée de vie ; le passage d’une branche conditionnelle à une autre peut également créer des identités distinctes, même si les deux branches contiennent le même type de vue. Apple explique ces relations dans Démystifier SwiftUI.

Pour une feuille pilotée par un booléen, le contrôle de présentation documenté est la liaison transmise à sheet(isPresented:onDismiss:content:) : la valeur true demande la présentation. Pour rechercher la cause d’une feuille qui disparaît, vérifiez à la fois cette liaison et la présence continue de la vue qui porte le modificateur. Une valeur true stockée ailleurs ne garantit pas que l’hôte de présentation d’origine a survécu. Consultez la documentation d’Apple sur les feuilles pilotées par un booléen.

Une cause particulièrement directe est la modification d’un identifiant explicite. Apple indique qu’un changement de valeur fournie à id(_:) réinitialise l’identité de la vue, y compris son état. Examinez la vue présentatrice et ses ancêtres pour repérer les identifiants dérivés de compteurs de révision, de texte modifiable ou de valeurs nouvellement générées. Supprimez ces identifiants si leur comportement de réinitialisation n’est pas souhaité. Consultez id(_:).

La déduction pratique consiste à placer le modificateur de feuille à l’extérieur de la partie changeante de l’interface. Si une vue de chargement, une ligne filtrée ou une autre disposition des détails peut disparaître, son ancêtre persistant le plus proche est généralement un meilleur propriétaire de la présentation.

Créer un propriétaire stable pour la feuille avec un exemple Mac complet

Créez une app SwiftUI pour macOS dans Xcode, définissez macOS 12 ou une version ultérieure comme cible de déploiement, puis remplacez les déclarations générées de l’app et de la vue de contenu par le fichier unique ci-dessous. Ne gardez qu’une seule déclaration d’app @main. La version minimale macOS 12 permet d’utiliser l’action d’environnement dismiss de l’exemple ; TextEditor est disponible dès macOS 11. Consultez la documentation d’Apple sur la fermeture et celle sur TextEditor.

La vue racine de la fenêtre possède le booléen de présentation, le texte modifiable, le compteur de révision et le booléen de disposition. Son VStack externe porte le modificateur de feuille. Seul le contenu à l’intérieur de ce conteneur change de branche. Apple recommande de déclarer l’état comme privé dans la vue la plus élevée qui en a besoin et de transmettre des liaisons aux vues enfants qui doivent le modifier. Consultez 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)
    }
}

Il s’agit d’une app de démonstration complète, examinée au regard des API documentées. Elle n’a pas été compilée ni exécutée ici, car cet environnement ne dispose ni d’une chaîne d’outils Swift ni d’un environnement d’exécution macOS.

Vérifier la mise à jour sans fermer la feuille

  1. Lancez l’app et cliquez sur Edit Note.
  2. Remplacez le texte d’exemple par Sample note Beta.
  3. Cliquez plusieurs fois sur Update Owner. Chaque clic incrémente l’état du propriétaire et change la vue conditionnelle de celui-ci.
  4. Vérifiez que l’éditeur reste visible, que son texte reste Sample note Beta et que sa révision affichée augmente.
  5. Cliquez sur Done. Vérifiez que la fenêtre affiche le texte modifié dans la disposition développée lorsque celle-ci est active.
  6. Rouvrez l’éditeur et vérifiez que le texte modifié est toujours disponible.

Le bouton de mise à jour se trouve dans la feuille afin que le test ne dépende pas d’un clic sur la fenêtre sous-jacente. AppKit décrit les feuilles comme étant modales pour le document : lorsqu’une feuille est visible, la plupart des événements dirigés vers sa fenêtre parente sont interdits. Mettre à jour le propriétaire au moyen d’une liaison permet à cet exemple de tester directement le changement d’état. Consultez beginSheet(_:completionHandler:).

Définir ce que doit signifier la fermeture de l’éditeur

Cet exemple modifie immédiatement le texte du propriétaire. Done ferme l’éditeur ; cette action n’effectue pas d’enregistrement distinct. La chaîne reste dans l’état local de la fenêtre, et l’exemple ne prévoit aucune persistance entre les lancements de l’app. Ce comportement découle de la conception à liaison partagée décrite dans State.

Si votre tâche nécessite un brouillon annulable, utilisez une valeur de brouillon distincte et copiez-la dans le document uniquement à la suite d’une action de confirmation explicite. Définissez ce comportement avant d’implémenter la fermeture : décider de valider ou d’abandonner un brouillon est indépendant du choix de la vue propriétaire de la feuille.

Diagnostiquer la fermeture avec trois vérifications indépendantes

Suivez la séquence ci-dessous dans votre propre app. Elle transforme les règles générales d’identité en une enquête ciblée, sans supposer que toutes les feuilles qui disparaissent ont la même cause.

Vérification Élément à examiner Correction pratique
Valeur de présentation Chaque écriture qui définit le booléen sur false ou efface l’élément de présentation facultatif Réservez les écritures de fermeture aux actions de fermeture voulues
Identité du présentateur Branches conditionnelles, lignes supprimées, identifiants d’ancêtres qui changent Déplacez à la fois l’état de présentation et le modificateur vers un ancêtre stable approprié
Durée de vie du contenu État local à la feuille, identifiants du contenu qui changent, synchronisation de l’éditeur Préservez la session de modification voulue et mettez à jour délibérément le contenu existant

Commencez par rechercher dans votre projet les affectations à la propriété de présentation et les appels à dismiss(). Un rappel d’actualisation ferme peut-être explicitement la feuille. Lisez dismiss dans la propre vue de la feuille, comme dans l’exemple : Apple précise que l’action s’applique à l’environnement dans lequel elle est obtenue. La lire dans la vue racine de la fenêtre peut cibler cette fenêtre à la place. Consultez dismiss.

Ensuite, examinez la hiérarchie au-dessus du modificateur. Si la feuille est attachée à une vue de détails qu’une actualisation supprime, déplacez-la au-dessus de cette limite de remplacement. Déplacez également l’état de présentation qui lui appartient localement. Selon l’explication d’Apple sur la durée de vie des vues, conserver l’indicateur dans une vue enfant dont l’identité prend fin expose son stockage au remplacement.

Enfin, distinguez une présentation fermée d’un éditeur qui reste visible mais perd son brouillon. Commencez avec l’exemple simple de TextEditor, puis réintroduisez progressivement votre contenu personnalisé. Cette comparaison est une méthode de diagnostic : elle aide à déterminer si l’échec vient de la propriété de la présentation ou de votre logique de mise à jour du contenu.

Faut-il passer à une feuille pilotée par un élément ?

Utilisez sheet(item:onDismiss:content:) lorsqu’une entité sélectionnée particulière est à l’origine de la présentation. Apple décrit un élément facultatif conforme à Identifiable, transmet un élément non nil à la fermeture de contenu et précise que l’élément affiché est remplacé lorsqu’il change. Changer de surcharge ne permet pas à lui seul de préserver un présentateur dont la durée de vie prend fin. Consultez la documentation sur les feuilles pilotées par un élément.

Choisissez un identifiant qui représente l’entité pendant toute la session de modification prévue. Le protocole Swift Identifiable exprime une identité stable, et sa documentation précise explicitement que les types concernés définissent la durée et la portée requises. Un nouvel identifiant généré à chaque lecture d’une propriété ne peut pas représenter une entité persistante. Consultez Identifiable.

Pour un éditeur simple comme celui de la démonstration, un indicateur de présentation booléen associé à des liaisons de texte distinctes rend la relation voulue facile à examiner. Pour un éditeur piloté par un élément sélectionné, séparez l’identité de la sélection des champs qui changent fréquemment. Considérez la sélection d’une autre entité comme une décision délibérée de présentation et définissez d’abord ce qu’il adviendra du brouillon actuel.

Désactiver la fermeture interactive gardera-t-il la feuille active ?

interactiveDismissDisabled(_:) contrôle la fermeture non programmée. Apple précise explicitement que cela n’empêche pas la fermeture provoquée par un changement de la liaison de présentation ou par l’appel de dismiss. Son rôle documenté ne permet donc pas de corriger le remplacement d’identité. Consultez interactiveDismissDisabled(_:).

Appliquez le modificateur à l’intérieur de la hiérarchie de la feuille uniquement si votre interface nécessite ce comportement de fermeture, et prévoyez une action explicite pour terminer. Pour cette tâche, commencez par préserver l’hôte et son état. Ajouter une restriction de fermeture avant d’avoir identifié la cause peut laisser le problème initial de propriété sans solution.

Que change la présence de contenu AppKit ou TextKit dans la feuille ?

Pour intégrer un NSTextView avec NSViewRepresentable, séparez la création des mises à jour. Apple définit makeNSView(context:) comme l’étape de création et updateNSView(_:context:) comme l’endroit où appliquer les informations SwiftUI ultérieures. Un coordinateur transmet à SwiftUI les modifications du délégué ou de la cible-action. Consultez NSViewRepresentable et makeNSView(context:).

Une règle de mise en œuvre utile, déduite de ce cycle de vie, consiste à éviter de recréer l’éditeur simplement parce que l’état sans rapport du propriétaire a changé. Avant de remplacer son contenu, déterminez si le texte du modèle entrant diffère réellement et comment gérer les modifications locales simultanées. Une feuille stable ne règle pas à elle seule les écritures concurrentes dans son texte.

TextKit fournit le stockage, la mise en page et le rendu du texte. L’NSTextView d’AppKit regroupe ces fonctionnalités et expose des moteurs de mise en page modernes et anciens. Ces mécanismes concernent le contenu de l’éditeur ; la propriété de la présentation nécessite toujours une durée de vie stable qui lui soit propre. Utilisez TextKit lorsque la tâche de modification l’exige et examinez séparément le comportement de mise à jour du texte et la fermeture de la feuille. Consultez la présentation de TextKit par Apple.

Quand la présentation AppKit possédée par la fenêtre est-elle appropriée ?

Si votre app existante gère déjà le cycle de vie de sa fenêtre dans AppKit, macOS 26 introduit NSWindow.beginSheet(content:completionHandler:) pour présenter directement du contenu SwiftUI dans cette fenêtre. Apple documente la prise en charge de l’action d’environnement de fermeture de la feuille, du dimensionnement, des barres d’outils et du contrôle de fermeture interactive. Utilisez un SDK macOS 26 et respectez la disponibilité de macOS 26 avant de l’adopter. Consultez beginSheet(content:completionHandler:).

L’objet NSWindow.HostingSheetRepresentation retourné peut modifier la vue racine présentée ou permettre une fermeture programmée depuis AppKit. C’est une option documentée lorsque la propriété au niveau de la fenêtre s’accorde à l’architecture de l’app. Elle n’est pas nécessaire pour l’exemple de conteneur SwiftUI stable ci-dessus. Consultez NSWindow.HostingSheetRepresentation.

Pour les versions antérieures de macOS, beginSheet(_:completionHandler:), déjà disponible dans AppKit, accepte une feuille NSWindow et documente la mise en file d’attente des présentations. Ne supposez pas que la nouvelle surcharge qui accepte du contenu SwiftUI est disponible sur ces systèmes. Consultez beginSheet(_:completionHandler:).

Gardez le propriétaire stable, maintenez active la valeur de présentation et préservez la source de vérité prévue pour l’éditeur. Vérifiez ces conditions avec une petite action locale de mise à jour avant de rétablir un contenu plus complexe. Vous disposerez ainsi d’un moyen concret de continuer la modification pendant la mise à jour de l’interface, dans les limites de durée de vie et d’API documentées par Apple.

Sources