Pour qu’un NSTextView créé par programmation accepte la saisie, activez l’édition, ajoutez-le à votre fenêtre et appelez makeFirstResponder(_:) sur cette fenêtre. La fenêtre doit également pouvoir devenir la fenêtre clé. Voici un exemple complet et une procédure de dépannage ciblée.
Quelles conditions faut-il réunir pour que la saisie fonctionne ?
Ce guide porte sur une seule tâche : créer une petite fenêtre d’application Mac dans laquelle une personne peut saisir et modifier du texte multiligne. Il utilise directement AppKit, puis explique les choix de cycle de vie correspondants lorsque l’éditeur est intégré à SwiftUI.
Trois conditions distinctes sont importantes. La vue de texte doit autoriser l’édition, la fenêtre doit recevoir les événements clavier et la vue de texte doit être le premier répondant souhaité dans cette fenêtre. Apple identifie la keyWindow de l’application comme la fenêtre qui reçoit actuellement les événements clavier ; afficher une fenêtre ne détermine pas à lui seul quelle vue doit permettre de modifier le texte. Apple : keyWindow
Définir isEditable = true autorise l’édition et rend également la vue de texte sélectionnable. Apple indique que les vues de texte sont modifiables par défaut, mais définir explicitement cette propriété clarifie la configuration de l’exemple. Le fait que le texte soit modifiable ne détermine pas à lui seul la destination du clavier. Apple : isEditable
Pour définir le focus, utilisez la méthode makeFirstResponder(textView) de la fenêtre conteneur. Apple indique explicitement aux développeurs d’utiliser cette méthode plutôt que d’appeler directement la méthode becomeFirstResponder() de la vue de texte. La fenêtre peut ainsi coordonner la transition entre les répondants. Apple : NSTextView
Créer une fenêtre modifiable complète en Swift
Le programme autonome suivant cible macOS 14 ou une version ultérieure et utilise une version actuelle de Swift avec le SDK macOS. Le titre fictif de la fenêtre est Local Editor et son texte initial est Sample note. Le texte reste en mémoire ; cet exemple n’implémente pas l’enregistrement.
Enregistrez le programme complet sous le nom EditorDemo.swift. Utilisez-le comme exécutable autonome plutôt que d’ajouter un second point d’entrée à une application existante. Swift n’autorise qu’un seul point d’entrée pour un programme ; le type @main ci-dessous le fournit. The Swift Programming Language : Attributes
import AppKit
@MainActor
final class EditorDelegate: NSObject, NSApplicationDelegate {
private var editorWindow: NSWindow?
func applicationDidFinishLaunching(_ notification: Notification) {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 640, height: 420),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.title = "Local Editor"
window.isReleasedWhenClosed = false
let scrollView = NSTextView.scrollableTextView()
guard let textView = scrollView.documentView as? NSTextView else {
fatalError("Expected an NSTextView document view")
}
textView.isEditable = true
textView.isSelectable = true
textView.isRichText = false
textView.allowsUndo = true
textView.font = NSFont.systemFont(ofSize: 16)
textView.textContainerInset = NSSize(width: 12, height: 12)
textView.string = "Sample note\n"
textView.setSelectedRange(
NSRange(location: (textView.string as NSString).length, length: 0)
)
scrollView.frame = NSRect(x: 0, y: 0, width: 640, height: 420)
scrollView.autoresizingMask = [.width, .height]
window.contentView = scrollView
window.initialFirstResponder = textView
editorWindow = window
let mainMenu = NSMenu()
let appMenuItem = NSMenuItem()
mainMenu.addItem(appMenuItem)
let appMenu = NSMenu()
appMenu.addItem(NSMenuItem(
title: "Quit Local Editor",
action: #selector(NSApplication.terminate(_:)),
keyEquivalent: "q"
))
appMenuItem.submenu = appMenu
NSApp.mainMenu = mainMenu
window.center()
window.makeKeyAndOrderFront(nil)
NSApp.activate()
guard textView.window === window,
textView.acceptsFirstResponder else {
print("Editor is not ready to receive focus")
return
}
let accepted = window.makeFirstResponder(textView)
print("Focus request returned:", accepted)
print("Editor is first responder:", window.firstResponder === textView)
print("Window is key:", window.isKeyWindow)
}
func applicationShouldTerminateAfterLastWindowClosed(
_ sender: NSApplication
) -> Bool {
true
}
}
@main
@MainActor
enum EditorDemo {
static func main() {
let application = NSApplication.shared
application.setActivationPolicy(.regular)
let delegate = EditorDelegate()
application.delegate = delegate
withExtendedLifetime(delegate) {
application.run()
}
}
}
Sur un Mac où Xcode ou les outils en ligne de commande sont installés, compilez et lancez le programme depuis le répertoire contenant le fichier :
xcrun swiftc -parse-as-library \
-target "$(uname -m)-apple-macos14.0" \
-framework AppKit EditorDemo.swift -o EditorDemo
./EditorDemo
L’exemple a été vérifié à l’aide de la documentation récupérée, mais n’a pas pu être compilé ni exécuté dans cet environnement de recherche, qui fonctionne sous Linux et ne dispose pas de compilateur Swift. Vérifiez le comportement de la fenêtre sous macOS à l’aide des contrôles ci-dessous.
Pourquoi cette configuration permet la saisie au clavier
La méthode de commodité scrollableTextView() renvoie un NSScrollView ; le code récupère sa vue de texte avec documentView. Placer l’éditeur de texte dans une vue de défilement fournit à ce petit exemple un conteneur simple pour le contenu multiligne. Cette fabrique est disponible depuis macOS 10.14. Apple : scrollableTextView()
Le code ajoute ce conteneur avant de demander le focus. Il définit également initialFirstResponder, qu’Apple décrit comme la vue sélectionnée lors de la première apparition de la fenêtre. L’appel ultérieur à makeFirstResponder(_:) demande explicitement le focus pour l’éditeur et fournit un résultat à vérifier. Le focus initial et une demande ultérieure de focus correspondent à des étapes différentes du cycle de vie de la fenêtre. Apple : initialFirstResponder, Apple : makeFirstResponder(_:)
La fenêtre utilise un style avec barre de titre afin de pouvoir devenir la fenêtre clé. makeKeyAndOrderFront(nil) demande à la fois l’affichage de la fenêtre et son passage au statut de fenêtre clé. Apple distingue cette méthode de orderFront(_:), qui modifie l’ordre d’affichage sans changer la fenêtre clé. Apple : canBecomeKey, Apple : makeKeyAndOrderFront(_:)
L’appel à activate() est la raison pour laquelle ce programme nécessite macOS 14 ou une version ultérieure. Apple décrit l’activation comme une demande, et non comme une garantie. Si la fenêtre apparaît sans accepter la saisie, activez l’application en cliquant sur sa fenêtre, puis vérifiez à nouveau. Ne considérez pas un message de diagnostic immédiat comme la preuve que l’activation est terminée. Apple : activate()
Le point d’entrée et le délégué sont marqués @MainActor, dont Apple définit l’exécuteur comme équivalent à la file principale de distribution. Le programme démarre également la boucle d’événements d’AppKit avec run(), ce qui permet à AppKit de distribuer les événements utilisateur une fois la configuration terminée. Apple : MainActor, Apple : run()
Diagnostiquer un éditeur visible qui n’accepte toujours pas la saisie
Suivez les étapes ci-dessous dans l’ordre. Elles distinguent l’ajout à la fenêtre, l’éligibilité de celle-ci, la sélection du répondant et la possibilité de modifier le texte ; chaque constat indique ainsi l’action suivante à effectuer.
| Vérification | Signification du résultat | Action suivante |
|---|---|---|
textView.window === window |
L’éditeur appartient à la fenêtre souhaitée. | Si le résultat est faux, terminez l’ajout de la hiérarchie de vues avant de demander le focus. |
window.canBecomeKey |
La fenêtre peut devenir la fenêtre clé. | Si le résultat est faux, examinez son style ou sa sous-classe personnalisée. |
window.isKeyWindow |
Cette fenêtre est actuellement la fenêtre clé de l’application. | Activez l’application et sélectionnez la fenêtre souhaitée, puis vérifiez à nouveau. |
textView.acceptsFirstResponder |
L’éditeur accepte une demande de focus. | Si le résultat est faux, examinez sa configuration et les redéfinitions de ses méthodes dans les sous-classes. |
window.firstResponder === textView |
Cet éditeur précis est le premier répondant. | Si le résultat est faux, examinez la transition de focus et le répondant actuel. |
textView.isEditable |
La modification par l’utilisateur est activée. | Définissez cette propriété sur true si l’édition est souhaitée. |
La vérification du répondant est particulièrement utile, car makeFirstResponder(_:) présente une subtilité documentée : cette méthode peut renvoyer true lorsque le répondant demandé refuse le statut de premier répondant et que la fenêtre devient le premier répondant à sa place. Apple recommande également de vérifier acceptsFirstResponder avant de faire explicitement la demande. Il faut donc examiner la valeur de retour et l’identité de firstResponder après l’appel. Apple : makeFirstResponder(_:)
Un résultat false peut signifier que le premier répondant actuel a refusé de céder sa place. Examinez le comportement de ce répondant et tout délégué d’édition impliqué. Par exemple, Apple indique que le délégué d’une vue de texte peut refuser la fin de l’édition via textShouldEndEditing(_:). Répéter la même demande de focus sans résoudre le refus n’en traite pas la cause. Apple : makeFirstResponder(_:), Apple : NSTextView
Pour une vérification manuelle simple, saisissez Blue square, appuyez sur Retour, saisissez Green circle, puis utilisez les touches fléchées et la touche Suppr pour modifier le texte. Redimensionnez la fenêtre et poursuivez la saisie. Il s’agit d’étapes de vérification proposées, et non de résultats de tests rapportés. Fermez la fenêtre une fois terminé ; l’exemple se termine alors.
Qu’est-ce qui change pour une fenêtre sans bordure ?
Une fenêtre sans bordure visible peut ne pas pouvoir devenir la fenêtre clé. Apple indique qu’un NSWindow ordinaire sans le style avec barre de titre ne devient pas la fenêtre clé par défaut. La personnalisation prise en charge consiste à redéfinir canBecomeKey dans une sous-classe de NSWindow et à renvoyer true lorsque cette fenêtre doit recevoir les événements clavier. Apple : canBecomeKey
Commencez par l’exemple avec barre de titre pour établir que la saisie de texte fonctionne. Si l’interface nécessite ensuite une fenêtre sans bordure, modifiez l’implémentation de la fenêtre en conservant les mêmes vérifications concernant l’ajout à la fenêtre, la possibilité de modifier le texte et le répondant. Modifier l’apparence de la fenêtre ne remplace aucune de ces exigences.
Comment faire fonctionner le même éditeur dans SwiftUI ?
Utilisez NSViewRepresentable pour intégrer une vue de texte AppKit dans une hiérarchie SwiftUI. Apple confie la création et la configuration initiale à makeNSView(context:), les mises à jour continues de l’état à updateNSView(_:context:), et la coordination des messages de délégué ou de cible-action à un coordinateur. Apple : NSViewRepresentable
Pour cet éditeur, une conception pratique consiste à renvoyer la vue de défilement comme vue représentée et à conserver une référence à la vue de texte du document dans le coordinateur ou dans un conteneur personnalisé. Configurez la possibilité de modifier le texte lors de la création. Transmettez les changements de texte de l’utilisateur via le délégué lorsque l’interface SwiftUI environnante a besoin d’une liaison.
Considérez l’ajout à la fenêtre comme un événement distinct de la création. Avant de demander le focus, vérifiez que le window de l’éditeur n’est pas nil. Une sous-classe AppKit peut utiliser viewDidMoveToWindow() pour observer les changements dans la hiérarchie ; Apple indique qu’un window nil à cet endroit signifie que la vue ne se trouve actuellement dans aucune fenêtre. Cela fournit un point d’ancrage de cycle de vie documenté pour implémenter une demande de focus tenant compte de l’ajout à la fenêtre. Apple : viewDidMoveToWindow()
Comme règle d’implémentation, demandez le focus initial une seule fois pour la présentation souhaitée, ou en réponse à une action explicite telle que Focus Editor. Évitez d’appeler systématiquement makeFirstResponder(_:) à chaque mise à jour de SwiftUI : cette règle ramènerait sans cesse le focus à l’éditeur après que la personne a sélectionné un autre contrôle. Vérifiez la fenêtre conteneur au moment de la demande plutôt que de supposer qu’une fenêtre stockée précédemment reste appropriée.
Évitez également de réinitialiser la chaîne de l’éditeur à chaque mise à jour. Dans une enveloppe fondée sur une liaison, comparez l’état reçu au texte actuel et tenez compte du texte marqué et de la sélection avant de remplacer le contenu. L’exemple autonome initialise sa chaîne une seule fois ; un éditeur synchronisé nécessite une règle de mise à jour explicite.
Laissez SwiftUI dimensionner la vue racine représentée. Apple avertit que la modification directe de frame ou de bounds de cette vue entre en conflit avec la gestion de la mise en page de SwiftUI. La configuration directe du cadre dans le programme AppKit autonome n’est donc pas une méthode de dimensionnement à reprendre pour une vue racine gérée par SwiftUI. Apple : NSViewRepresentable
La saisie nécessite-t-elle une pile TextKit personnalisée ?
Pour cette tâche, utilisez le comportement d’édition déjà fourni par NSTextView. Apple le décrit comme l’interface frontale du système de texte qui dessine le texte et gère la sélection, la modification, la gestion des entrées et les raccourcis clavier. La saisie ordinaire ne nécessite pas de reconstruire ces fonctionnalités. Apple : NSTextView
Le choix du moteur TextKit est une décision distincte du focus clavier. Apple indique que init(frame:) utilise NSLayoutManager par défaut, tandis qu’une vue de texte assemblée manuellement peut utiliser NSTextLayoutManager sur macOS 12 ou une version ultérieure grâce à l’initialiseur désigné. Choisissez un moteur en fonction de vos besoins de mise en page ; changer de moteur ne remplace pas la nécessité de faire de l’éditeur le premier répondant. Apple : init(frame:textContainer:)
Un diagnostic mérite une attention particulière : lire layoutManager n’est pas nécessairement une simple inspection passive. Apple indique que l’accès à cette propriété sur une vue de texte ou un conteneur de texte utilisant le nouveau système peut déclencher un mode de compatibilité qui utilise NSLayoutManager. L’exemple évite de l’interroger et ne suppose pas quel moteur est choisi par la fabrique de vues de texte défilantes. Apple : NSTextView
Le résultat attendu est concret : la fenêtre souhaitée de l’application est active et clé, sa vue de texte modifiable est le premier répondant et la saisie change le texte affiché. Commencez par établir ce comportement dans la petite fenêtre, puis conservez les mêmes vérifications lorsque vous ajoutez l’intégration SwiftUI, une mise en page personnalisée ou d’autres contrôles.
Sources
- NSTextView — Apple.
- isEditable — Apple.
- keyWindow — Apple.
- makeFirstResponder(_:) — Apple.
- initialFirstResponder — Apple.
- makeKeyAndOrderFront(_:) — Apple.
- canBecomeKey — Apple.
- scrollableTextView() — Apple.
- activate() — Apple.
- MainActor — Apple.
- run() — Apple.
- NSViewRepresentable — Apple.
- viewDidMoveToWindow() — Apple.
- init(frame:textContainer:) — Apple.
- Attributes — The Swift Project.