Créez un éditeur Mac en conservant le texte validé dans la vue parente et en modifiant un brouillon distinct dans une feuille. Seule l’action Commit recopie le brouillon ; Cancel ferme la feuille. Ce tutoriel met en œuvre ce flux pour une petite note en texte brut dans votre propre application.
Définir le comportement de la validation et de l’annulation
La tâche de l’utilisateur est simple : ouvrir une note existante, en réviser plusieurs lignes, puis accepter la révision complète ou laisser l’original intact. L’exemple utilise du texte fictif et une seule session de modification. Il conserve le texte validé en mémoire ; l’ajout d’un stockage sur fichier relève d’une décision distincte pour l’application.
L’élément TextEditor d’Apple affiche du texte multiligne avec défilement et accepte une liaison vers la valeur en cours de modification. Une liaison vers la valeur validée permettrait donc aux modifications de changer cette valeur immédiatement. Pour différer la validation, liez plutôt l’éditeur à un brouillon. Consultez la documentation d’Apple sur TextEditor.
La conception comporte trois éléments : le texte validé détenu par la vue parente, un instantané créé au début de la modification et l’état modifiable du brouillon détenu par la feuille. Le type String de Swift a une sémantique de valeur : modifier le brouillon ne change donc pas la chaîne validée. Son stockage avec copie à l’écriture est une optimisation d’implémentation ; le comportement de valeur indépendante reste le même. Consultez String.
| Événement | Comportement du brouillon | Texte validé |
|---|---|---|
| Ouvrir l’éditeur | Part du texte validé actuel | Inchangé |
| Saisir ou supprimer du texte | Est modifié dans la feuille | Inchangé |
Choisir Commit |
Est transmis à la vue parente, puis la feuille se ferme | Remplacé par le brouillon |
Choisir Cancel |
La feuille se ferme sans transmettre le brouillon | Inchangé |
| Rouvrir l’éditeur | Démarre une nouvelle session | Fournit le nouveau texte initial |
Ce tableau constitue également le contrat d’implémentation. La fermeture de la feuille ne vaut pas validation. En explicitant cette distinction, l’annulation fonctionne sans opération de restauration.
Créer l’exemple macOS complet
Dans Xcode, créez un projet d’application macOS avec SwiftUI et définissez la cible de déploiement sur macOS 12 ou une version ultérieure. Remplacez les déclarations générées de l’application et de la vue de contenu par le fichier source unique suivant, en supprimant l’ancienne déclaration de l’application afin que la cible ne contienne qu’un seul point d’entrée @main. L’exemple utilise l’édition de texte brut plutôt que l’initialiseur plus récent pour le texte enrichi.
La version minimale macOS 12 est requise par DismissAction de la feuille ; TextEditor en texte brut est disponible à partir de macOS 11. Apple documente ces exigences de disponibilité dans DismissAction et TextEditor.
import SwiftUI
import Foundation
@main
struct DraftEditorApp: App {
var body: some Scene {
WindowGroup {
NoteView()
}
}
}
private struct EditSession: Identifiable {
let id = UUID()
let initialText: String
}
private struct NoteView: View {
@State private var acceptedText = """
Sample note
Arrange the blue tiles beside the green tiles.
"""
@State private var editSession: EditSession?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Accepted note")
.font(.headline)
ScrollView {
Text(acceptedText.isEmpty ? "(Empty note)" : acceptedText)
.frame(maxWidth: .infinity, alignment: .leading)
}
Button("Edit Note") {
editSession = EditSession(initialText: acceptedText)
}
}
.padding(20)
.frame(minWidth: 460, minHeight: 300)
.sheet(item: $editSession) { session in
DraftEditor(initialText: session.initialText) { newText in
acceptedText = newText
}
.id(session.id)
}
}
}
private struct DraftEditor: View {
@Environment(\.dismiss) private var dismiss
@State private var draft: String
private let initialText: String
private let onCommit: (String) -> Void
init(
initialText: String,
onCommit: @escaping (String) -> Void
) {
self.initialText = initialText
self.onCommit = onCommit
_draft = State(initialValue: initialText)
}
private var hasChanges: Bool {
draft != initialText
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Edit note")
.font(.headline)
TextEditor(text: $draft)
.font(.body)
.accessibilityLabel("Note draft")
.frame(minHeight: 220)
Text(hasChanges ? "Draft changed" : "No changes")
.font(.caption)
.foregroundColor(.secondary)
HStack {
Spacer()
Button("Cancel") {
dismiss()
}
.keyboardShortcut(.cancelAction)
Button("Commit") {
onCommit(draft)
dismiss()
}
.keyboardShortcut(.return, modifiers: .command)
}
}
.padding(20)
.frame(minWidth: 480, minHeight: 340)
}
}
Il s’agit d’un exemple d’application complet, avec son texte initial, son état de présentation, son éditeur et son point d’entrée. Le compilateur Swift et le SDK macOS n’étaient pas disponibles dans l’environnement de recherche ; l’exemple a donc été examiné à l’aune de la documentation de l’API récupérée, mais n’a pas été compilé ni exécuté ici.
Pourquoi la vue parente détient la valeur validée
La vue parente détient acceptedText parce qu’elle affiche le résultat validé et fournit la valeur initiale de la prochaine session de modification. La vue enfant détient draft parce que seule l’interface de modification doit pouvoir le changer. Cela suit les recommandations d’Apple, qui préconisent de garder l’état privé et de le placer au niveau approprié de la hiérarchie des vues. Consultez State.
La vue parente ne transmet pas $acceptedText à l’éditeur. Elle transmet plutôt une valeur de type chaîne et une fermeture. Cette fermeture contient la seule affectation qui valide une révision : acceptedText = newText. Dans cette implémentation, ni la saisie ni l’annulation ne peuvent atteindre cette affectation.
Pourquoi chaque ouverture reçoit un identifiant de session
L’élément EditSession facultatif fournit à la fois l’état de présentation et le texte initial. Apple indique que sheet(item:onDismiss:content:) présente le contenu lorsque l’élément lié n’est pas nil, puis transmet cet élément à la fermeture de contenu. Consultez sheet(item:onDismiss:content:).
Chaque pression sur Edit Note crée un nouvel identifiant. Appliquer cet identifiant à l’éditeur rend explicite la durée de vie visée par l’état : un brouillon appartient à une seule session de modification. L’initialiseur fournit la valeur initiale de l’état ; il ne sert pas à synchroniser en continu un brouillon existant avec les modifications ultérieures de la vue parente.
Pour cette tâche, ce comportement d’instantané est souhaitable. Une note partiellement modifiée ne devrait pas être réinitialisée soudainement à cause d’une mise à jour sans rapport dans une autre partie de la vue parente. Une nouvelle ouverture crée un brouillon vierge à partir du texte le plus récemment validé.
Maîtriser le comportement de fermeture et du clavier
Lisez @Environment(\.dismiss) dans l’éditeur présenté, comme dans l’exemple. Apple explique que cette action s’applique à l’environnement où elle est déclarée. La lire dans la vue racine qui présente la feuille peut viser la fenêtre de cette vue plutôt que la feuille. Consultez DismissAction.
Le bouton Cancel ferme simplement la feuille. Le brouillon peut disparaître puisqu’il n’a jamais modifié la valeur validée. Le bouton Commit appelle d’abord la fermeture de validation, puis ferme la feuille. Ne déplacez pas la validation dans onDismiss : ce rappel signale une fermeture, qui a également lieu en cas d’annulation.
L’exemple associe la touche Échap à Cancel avec .cancelAction, le raccourci standard d’annulation documenté par Apple. Il associe Commande-Retour à Commit au moyen d’un équivalent clavier et d’un modificateur explicites. Ces raccourcis contrôlent l’interface de l’application. Consultez cancelAction et keyboardShortcut(_:modifiers:).
La touche Retour reste disponible pour l’édition multiligne, car l’exemple n’associe pas .defaultAction au bouton de validation. Apple définit .defaultAction comme la touche Retour sans modificateur. Commande-Retour est un choix éditorial pour cette interface multiligne ; cela ne signifie pas que tous les éditeurs Mac devraient l’utiliser. Consultez defaultAction.
Veillez à ce que les raccourcis soient uniques dans l’interface concernée. Apple précise que si plusieurs contrôles ont le même raccourci, le premier contrôle correspondant trouvé lors de la résolution est utilisé. Testez les raccourcis avec le point d’insertion dans l’éditeur, ainsi qu’en cliquant sur les boutons visibles. Consultez keyboardShortcut(_:modifiers:).
Vérifier la limite de validation avec une courte séquence de tests
Effectuez les vérifications suivantes dans l’application Mac en cours d’exécution. Elles mettent à l’épreuve le comportement qui distingue un éditeur de brouillon d’un éditeur à modification directe, y compris le cas de la chaîne vide, facile à oublier.
- Ouvrez
Edit Note, remplacez la deuxième ligne parArrange the green tiles in a row., puis cliquez surCancel. La note validée doit conserver sa deuxième ligne d’origine. - Rouvrez l’éditeur. Il doit contenir le texte validé d’origine, sans la révision annulée.
- Effectuez la même révision et cliquez sur
Commit. La note validée doit maintenant afficher la ligne révisée. - Rouvrez l’éditeur, ajoutez un saut de ligne, puis utilisez Commande-Retour. Le brouillon multiligne complet doit devenir le texte validé.
- Rouvrez l’éditeur, modifiez le texte et testez Échap pendant la modification. Vérifiez que l’annulation laisse le texte validé inchangé.
- Rouvrez l’éditeur, supprimez tout le texte et cliquez sur
Commit. La vue parente doit afficher(Empty note). À la prochaine ouverture, l’éditeur doit être vide.
Dans cet exemple, un texte vide est valide. L’implémentation ne supprime ni les espaces ni les sauts de ligne, et ne désactive pas la validation lorsque la chaîne est vide. Si votre type de note exige du contenu, ajoutez une règle de validation visible et appliquez-la à l’action de validation ainsi qu’à l’état activé du bouton.
La comparaison hasChanges ne sert qu’à contrôler l’étiquette d’état. Laisser Commit activé en l’absence de modifications ne fait qu’affecter sans conséquence la même valeur. Si vous le désactivez plus tard lorsque le texte n’a pas changé, laissez Cancel disponible afin que la feuille propose toujours une sortie explicite.
Savoir quand SwiftUI suffit et quand AppKit peut aider
Pour cette tâche en texte brut, TextEditor fournit déjà la surface d’édition nécessaire. Ni une mise en page de texte personnalisée ni un pont vers AppKit ne sont requis pour séparer les modifications du brouillon du texte validé. Cette séparation relève de la conception de l’état de l’application.
SwiftUI prend également en charge aujourd’hui un initialiseur TextEditor pour les chaînes d’attributs, disponible à partir de macOS 26. Le texte enrichi n’impose donc pas automatiquement de remplacer SwiftUI. Il faut toutefois changer le type du brouillon et respecter la nouvelle version minimale requise. Apple documente également les limites de conversion des attributs de formatage pour cet éditeur. Consultez init(text:selection:).
Si vous avez besoin d’un comportement d’édition propre à AppKit ou d’un contrôle direct de la mise en page du texte, la méthode d’intégration prise en charge consiste à héberger un NSTextView via NSViewRepresentable. Apple décrit les méthodes de représentabilité permettant de créer, mettre à jour et démanteler une vue AppKit, avec un coordinateur qui renvoie à SwiftUI les modifications signalées par délégation. Consultez NSViewRepresentable.
Conservez le même contrat de validation dans ce pont : les modifications faites dans AppKit mettent à jour le brouillon, et seule l’action explicite de validation met à jour le modèle validé. Par précaution lors de l’implémentation, évitez de remplacer tout le contenu de la vue de texte à chaque mise à jour SwiftUI ; distinguez les modifications entrantes réelles des changements émis par l’éditeur lui-même.
TextKit présente une limite supplémentaire : NSTextView expose la mise en page moderne via textLayoutManager et la mise en page héritée via layoutManager. Apple indique que l’accès à cette dernière sous macOS 12 ou une version ultérieure fait passer la vue de texte en mode de compatibilité à l’aide de NSLayoutManager. Choisissez délibérément le moteur lorsque vous ajoutez des fonctionnalités de mise en page. Consultez TextKit et NSTextView.
Enfin, l’opération de validation de l’exemple est synchrone et effectuée en mémoire. Si vous la modifiez pour écrire dans un document, définissez si la validation signifie la mise à jour du modèle ou la fin de l’écriture. Pour une conception qui attend l’écriture avant la fermeture, laissez la feuille ouverte en cas d’échec et conservez le brouillon pour une nouvelle tentative. Si la même note peut être modifiée depuis une autre interface de votre application pendant la session, vérifiez sa révision avant de la remplacer.
La conception essentielle reste simple : créer un brouillon par session de modification, laisser l’éditeur le modifier et donner à la validation une seule voie explicite. Avec cette limite en place, l’annulation reste prévisible à mesure que la surface d’édition s’enrichit.
Sources
- TextEditor — Apple
- String — Apple
- State — Apple
- sheet(item:onDismiss:content:) — Apple
- DismissAction — Apple
- cancelAction — Apple
- defaultAction — Apple
- keyboardShortcut(_:modifiers:) — Apple
- init(text:selection:) — Apple
- NSViewRepresentable — Apple
- TextKit — Apple
- NSTextView — Apple