Utilisez le NSTokenField d’AppKit dans SwiftUI : renvoyez un objet personnalisé pour les libellés reconnus et une chaîne pour le texte ordinaire. Choisissez le style d’affichage pour chaque objet afin que les libellés connus apparaissent sous forme de jetons, tandis que le texte libre reste ordinaire.
Ce guide crée un éditeur de libellés compact dans votre propre app Mac. Son vocabulaire fictif contient Cedar et Slate ; les utilisateurs peuvent aussi saisir des expressions comme draft notes. L’éditeur communique son contenu à la fin de la modification. Il s’agit d’un champ pour des entrées mixtes, où les virgules servent de séparateurs, et non d’un éditeur de phrases qui conserve chaque signe de ponctuation.
Choisir le contrôle adapté à la tâche de saisie
Apple décrit NSTokenField comme un champ de texte qui convertit le texte saisi en jetons visuellement distincts. Son délégué peut fournir des objets représentés, des chaînes d’affichage, des chaînes modifiables et un style distinct pour chaque objet. Ces points d’extension constituent un point de départ pris en charge pour cette interface. NSTokenField, NSTokenFieldDelegate.
Faites une distinction avant de l’implémenter : accepter des entrées arbitraires et modifier un document arbitraire sont deux tâches différentes. Un champ de libellés séparés par des virgules peut utiliser celles-ci comme séparateurs. Un éditeur de modèles peut devoir conserver les virgules, les espaces et le texte à des positions exactes autour d’objets intégrés.
| Tâche de saisie | Point de départ approprié |
|---|---|
| Saisir des libellés reconnus parmi des entrées de texte arbitraires | NSTokenField avec des styles par objet |
| Saisir une chaîne ordinaire unique | SwiftUI TextField |
| Composer du texte continu avec des objets intégrés et une ponctuation exacte | NSTextView avec du texte enrichi et des pièces jointes |
SwiftUI présente TextField comme une interface de saisie liée à une valeur. NSViewRepresentable fournit le pont documenté permettant d’intégrer une vue AppKit dans SwiftUI. Pour l’édition de documents plus élaborée, NSTextView prend en charge le texte enrichi, les pièces jointes, la sélection, la gestion des entrées et le texte marqué. Ces fonctionnalités justifient les choix ci-dessus ; le tableau est une recommandation d’implémentation qui en découle. TextField, NSViewRepresentable, NSTextView.
Séparer l’identité des jetons du texte ordinaire
Utilisez un modèle ordonné comportant deux cas : un jeton reconnu et du texte littéral. Cela conserve une distinction qui disparaîtrait si vous aplatissez le tout en une seule chaîne d’affichage. Les tableaux Swift préservent l’ordre des éléments ; le guide d’Apple sur les champs à jetons précise que objectValue contient un tableau d’objets représentés, y compris des chaînes lorsque les valeurs représentées sont des chaînes. Array, Getting and Setting Token-Field Values.
Dans cet exemple, la reconnaissance est volontairement exacte et sensible à la casse. Cedar devient un jeton ; cedar reste du texte. La conversion ne supprime pas les espaces, ne met pas en minuscules, ne rejette pas et ne déduplique pas les entrées ordinaires. Cette règle rend l’exemple prévisible sans réécrire silencieusement ce qui a été saisi.
Les trois fichiers suivants constituent une app d’exemple complète. Dans Xcode, créez un projet d’app macOS utilisant SwiftUI et un SDK macOS actuel. Remplacez le point d’entrée généré de l’app par le troisième fichier et ajoutez les deux premiers au même target. Le code utilise les cas documentés actuels de TokenStyle, notamment .none et .rounded. Les exemples plus anciens peuvent employer d’autres formes ; vérifiez le SDK avec lequel vous compilez. NSTokenField.TokenStyle.
Définir le modèle et l’objet représenté
Placez ce code dans Entry.swift. L’objet immuable LabelToken est celui qu’AppKit reçoit pour un libellé reconnu. Entry est le modèle de valeur renvoyé à l’interface SwiftUI.
import Foundation
enum Entry: Equatable {
case token(String)
case text(String)
var summary: String {
switch self {
case .token(let value):
return "Token: \(String(reflecting: value))"
case .text(let value):
return "Text: \(String(reflecting: value))"
}
}
}
final class LabelToken: NSObject {
let label: String
init(_ label: String) {
self.label = label
super.init()
}
}
enum FieldReadError: Error {
case unexpectedValue
case unexpectedObject
}
Intégrer le champ natif dans SwiftUI
Placez ce code dans MixedEntryField.swift. Le type représentable initialise le contrôle une seule fois et actualise la fermeture de rappel lors des mises à jour SwiftUI. Son coordinateur implémente le délégué AppKit. Cela suit le cycle de vie documenté par Apple pour la création, la mise à jour et le coordinateur. SwiftUI contrôle la disposition de la vue représentée ; l’enveloppe ne définit donc pas son cadre. NSViewRepresentable.
import AppKit
import SwiftUI
@MainActor
struct MixedEntryField: NSViewRepresentable {
let initialEntries: [Entry]
var onCommit: (Result<[Entry], FieldReadError>) -> Void
func makeCoordinator() -> Coordinator {
Coordinator(onCommit: onCommit)
}
func makeNSView(context: Context) -> NSTokenField {
let field = NSTokenField()
field.delegate = context.coordinator
field.isEditable = true
field.isSelectable = true
field.placeholderString = "Cedar, Slate, or free text"
field.tokenizingCharacterSet = CharacterSet(charactersIn: ",")
field.setAccessibilityLabel("Labels and free text")
field.objectValue = initialEntries.map { entry -> Any in
switch entry {
case .token(let label):
return LabelToken(label)
case .text(let text):
return text
}
}
return field
}
func updateNSView(_ field: NSTokenField, context: Context) {
context.coordinator.onCommit = onCommit
}
final class Coordinator: NSObject, NSTokenFieldDelegate {
var onCommit: (Result<[Entry], FieldReadError>) -> Void
private let knownLabels: Set<String> = ["Cedar", "Slate"]
init(onCommit: @escaping
(Result<[Entry], FieldReadError>) -> Void) {
self.onCommit = onCommit
super.init()
}
func tokenField(
_ tokenField: NSTokenField,
representedObjectForEditing editingString: String
) -> Any? {
if knownLabels.contains(editingString) {
return LabelToken(editingString)
}
return editingString
}
func tokenField(
_ tokenField: NSTokenField,
styleForRepresentedObject object: Any
) -> NSTokenField.TokenStyle {
object is LabelToken ? .rounded : .none
}
private func label(for object: Any) -> String? {
if let token = object as? LabelToken {
return token.label
}
return object as? String
}
func tokenField(
_ tokenField: NSTokenField,
displayStringForRepresentedObject object: Any
) -> String? {
label(for: object)
}
func tokenField(
_ tokenField: NSTokenField,
editingStringForRepresentedObject object: Any
) -> String? {
label(for: object)
}
func controlTextDidEndEditing(_ notification: Notification) {
guard let field = notification.object as? NSTokenField else {
return
}
onCommit(readEntries(from: field))
}
private func readEntries(
from field: NSTokenField
) -> Result<[Entry], FieldReadError> {
guard let value = field.objectValue else {
return .success([])
}
guard let objects = value as? [Any] else {
return .failure(.unexpectedValue)
}
var entries: [Entry] = []
for object in objects {
if let token = object as? LabelToken {
entries.append(.token(token.label))
} else if let text = object as? String {
entries.append(.text(text))
} else {
return .failure(.unexpectedObject)
}
}
return .success(entries)
}
}
}
La méthode de conversion renvoie la chaîne de saisie d’origine pour une entrée non reconnue. Apple documente explicitement le renvoi de la chaîne de saisie lorsqu’il n’existe pas d’objet représenté personnalisé. Le délégué de style remplace le style global du champ pour chaque objet ; les libellés reconnus utilisent .rounded, et les chaînes, .none. Le guide d’architecture archivé d’Apple décrit également le remplacement des jetons encapsulés par des jetons en texte brut à l’aide de ce point d’extension du délégué. representedObjectForEditing, styleForRepresentedObject, How Token Fields Work.
Les deux méthodes de conversion de chaînes sont importantes. La méthode d’affichage fournit un libellé lisible pour un objet représenté. La méthode de modification fournit sa forme textuelle modifiable ; Apple indique qu’il faut renvoyer nil à cet endroit lorsqu’un jeton ne doit pas être modifiable. Cet exemple renvoie plutôt le libellé. displayStringForRepresentedObject, editingStringForRepresentedObject.
Ajouter une petite interface pour examiner les valeurs validées
Placez ce code dans MixedTokenDemoApp.swift. Ne conservez qu’une seule déclaration d’app @main dans le target. Le second champ de texte fournit une destination explicite pour déplacer le focus clavier hors du champ mixte.
import SwiftUI
@main
struct MixedTokenDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@MainActor
struct ContentView: View {
@State private var focusDestination = ""
@State private var output = "Finish editing to inspect the entries."
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Labels and free text")
.font(.headline)
MixedEntryField(
initialEntries: [.token("Cedar")]
) { result in
switch result {
case .success(let entries):
output = entries.isEmpty
? "No entries."
: entries.map(\.summary).joined(separator: "\n")
case .failure:
output = "Could not read all entries. The field was kept."
}
}
.frame(height: 28)
Text("Use commas between entries; then move to the next field.")
.font(.caption)
TextField("Move focus here to finish editing",
text: $focusDestination)
Text(output)
.font(.system(.body, design: .monospaced))
.textSelection(.enabled)
}
.padding(20)
.frame(minWidth: 540, minHeight: 250)
}
}
Valider les modifications sans perturber la saisie en cours
Cet exemple expose un instantané validé plutôt qu’une liaison qui change à chaque frappe. Apple définit controlTextDidEndEditing comme la notification indiquant que la modification est terminée et que les changements ont été validés. Lire les objets représentés à ce moment fournit à l’interface environnante un point clair où utiliser le résultat. controlTextDidEndEditing.
Évitez d’affecter objectValue sans condition depuis updateNSView. Apple indique que la définition de cette propriété pendant la modification d’une cellule interrompt la modification avant d’affecter la nouvelle valeur. Sa lecture pendant la modification déclenche également la validation. Ce sont des opérations qui ont des conséquences, et non une simple synchronisation passive. objectValue.
L’enveloppe traite donc initialEntries comme une valeur initiale. Modifier cet argument après la création ne recharge pas le champ. Si votre app doit ensuite remplacer les valeurs depuis l’extérieur, concevez une opération de remplacement explicite qui définit une règle pour les modifications en attente. Terminez et acceptez la modification en cours, ou abandonnez-la délibérément, avant de remplacer les valeurs représentées.
Le décodeur évite également compactMap : un objet représenté inconnu provoque un échec au lieu de disparaître du résultat. L’interface signale cet échec et laisse le champ intact. C’est important si vous étendez l’exemple avec d’autres types d’objets.
Vérifier le comportement des entrées mixtes sur les versions de macOS ciblées
Le code de ce document a été examiné à partir des déclarations d’API récupérées ; il n’a pas été compilé ni exécuté ici, car l’environnement disponible est Linux et ne dispose pas d’une chaîne d’outils Swift. Compilez et testez l’app sur un Mac avant de l’intégrer à une interface plus large.
Suivez cette séquence de diagnostic pour distinguer les problèmes de reconnaissance, de style et de validation :
- Vérifiez que l’objet initial
Cedara l’apparence d’un jeton. - Ajoutez
Slateet saisissez une virgule. Vérifiez que le libellé reconnu prend l’apparence d’un jeton. - Ajoutez
draft notes, puis déplacez le focus vers le second champ. Vérifiez que l’expression reste en texte brut et apparaît comme du texte dans le résultat validé. - Saisissez
cedaren minuscules. Vérifiez que cette entrée reste du texte conformément à la règle de correspondance exacte de l’exemple. - Remplacez un libellé reconnu existant par une expression inconnue, terminez la modification et vérifiez le type obtenu.
- Supprimez toutes les entrées et terminez la modification. Vérifiez que le résultat indique que le champ est vide.
- Essayez la sélection, la suppression, l’annulation et la composition de texte avec les méthodes de saisie que vous comptez prendre en charge. Vérifiez que les mises à jour SwiftUI sans rapport ne relancent pas la modification.
Dans cet exemple, la virgule est une syntaxe : elle sépare les entrées et n’est pas conservée comme contenu littéral. Les espaces ne sont pas configurés comme séparateurs. N’ajoutez pas d’autres caractères à la liste des séparateurs à la légère si les expressions doivent pouvoir les contenir. L’ensemble des séparateurs se configure avec tokenizingCharacterSet. tokenizingCharacterSet.
Ne vous contentez pas d’appuyer plusieurs fois sur Retour pour tester la validation. La description archivée d’Apple explique que Retour peut transformer la chaîne actuelle en jeton ou déclencher l’action du contrôle. Déplacer le focus vers un autre contrôle modifiable facilite l’examen de la fin de modification dans cet exemple. Les archives fournissent le contexte comportemental ; la documentation actuelle fournit les déclarations Swift utilisées ci-dessus. How Token Fields Work.
Quand faut-il plutôt utiliser TextKit ?
Choisissez la voie du système de texte plus riche lorsque le résultat requis est du texte continu contenant des jetons intégrés à des positions arbitraires. Par exemple, conserver la ponctuation exacte dans Arrange [Cedar], then add a note. nécessite un contrat de modification différent de celui qui consomme les virgules entre les entrées.
La base prise en charge est NSTextView avec du texte enrichi et des objets NSTextAttachment. Apple documente les pièces jointes comme des valeurs d’attribut dans une chaîne attribuée, avec une cellule de pièce jointe sur macOS pour le dessin et l’interaction avec la souris. Les capacités des pièces jointes basées sur des vues sont également documentées à partir de macOS 12. Ces API fournissent des briques de base ; elles ne définissent pas le modèle complet des jetons, la règle d’insertion ni le format d’exportation de votre app. NSTextView, NSTextAttachment.
Si vous choisissez cette voie, respectez la limite de TextKit : Apple avertit que l’accès explicite à layoutManager d’un NSTextView ou d’un conteneur de texte sur macOS 12 et versions ultérieures le fait passer en mode de compatibilité, qui utilise NSLayoutManager. Décidez du système de mise en page de texte que votre implémentation utilisera avant de combiner des exemples. NSTextView.
Pour un champ compact de libellés reconnus et d’entrées de texte libre, commencez par le champ natif à jetons et ses conversions de délégué. Conservez le texte ordinaire, définissez explicitement les séparateurs et lisez les objets représentés validés. Passez à un éditeur de document lorsque la structure exacte du texte intégré fait partie de la tâche.
Sources
- NSTokenField — Apple
- NSTokenFieldDelegate — Apple
- NSTokenField.TokenStyle — Apple
- tokenField(_:representedObjectForEditing:) — Apple
- tokenField(_:styleForRepresentedObject:) — Apple
- tokenField(_:displayStringForRepresentedObject:) — Apple
- tokenField(_:editingStringForRepresentedObject:) — Apple
- tokenizingCharacterSet — Apple
- NSViewRepresentable — Apple
- TextField — Apple
- objectValue — Apple
- controlTextDidEndEditing(_:) — Apple
- Array — Apple
- Getting and Setting Token-Field Values — Apple Documentation Archive
- How Token Fields Work — Apple Documentation Archive
- NSTextView — Apple
- NSTextAttachment — Apple