Gebruik AppKit's NSTokenField in SwiftUI, die een aangepast object retourneert voor herkende labels en een string voor gewone tekst. Kies de weergavestijl per object zodat bekende labels als tokens verschijnen terwijl vrije tekst gewoon blijft.
Deze handleiding bouwt een compact labeleditor in je eigen Mac-app. De synthetische woordenschat bevat Cedar en Slate; gebruikers kunnen ook zinnen invoeren zoals draft notes. De editor rapporteert de inhoud wanneer bewerken eindigt. Dit is een veld voor gemengde invoer, met komma's als scheidingstekens - geen zin-editor die elk leesteken behoudt.
Kies het controle-element dat bij de bewerkingstaak past
Apple beschrijft NSTokenField als een tekstveld dat ingevoerde tekst omzet in visueel verschillende tokens. De delegate kan vertegenwoordigde objecten, weergavestrings, bewerkbare strings en een afzonderlijke stijl per object leveren. Deze haken bieden het ondersteunde startpunt voor deze interface. NSTokenField, NSTokenFieldDelegate.
Maak een onderscheid voordat je het implementeert: willekeurige invoer accepteren en een willekeurig document bewerken zijn verschillende taken. Een kommagescheiden labelveld kan komma's als scheidingstekens consumeren. Een sjablooneditor moet misschien komma's, spaties en tekst op exacte posities rond inline-objecten behouden.
| Lezer's bewerkingstaak | Geschikt startpunt |
|---|---|
| Herkende labels invoeren naast willekeurige teksteinvoeren | NSTokenField met per-object stijlen |
| Een enkele gewone string invoeren | SwiftUI TextField |
| Continu tekst samenstellen met inline-objecten en exacte leestekens | NSTextView met geattribueerde tekst en bijlagen |
SwiftUI documenteert TextField als een bewerkbare interface gebonden aan een waarde. NSViewRepresentable levert de gedocumenteerde brug voor het plaatsen van een AppKit-weergave in SwiftUI. Voor rijker documentbewerking ondersteunt NSTextView opgemaakte tekst, bijlagen, selectie, invoerbeheer en gemarkeerde tekst. Deze mogelijkheden ondersteunen de bovenstaande keuzes; de tabel is een implementatie-aanbeveling afgeleid daarvan. TextField, NSViewRepresentable, NSTextView.
Houd token-identiteit gescheiden van gewone tekst
Gebruik een geordend model met twee gevallen: een herkende token en letterlijke tekst. Dit behoudt een onderscheid dat zou verdwijnen als je alles in één weergavestring platlegt. Swift-arrays behouden de elementvolgorde; Apple's token-field handleiding specificeert dat objectValue een array bevat van vertegenwoordigde objecten, inclusief strings wanneer strings de vertegenwoordigde waarden zijn. Array, Getting and Setting Token-Field Values.
Voor dit voorbeeld is herkenning bewust exact en hoofdlettergevoelig. Cedar wordt een token; cedar blijft tekst. De conversie knipt niet, maakt niet lowercase, verwijdert niet en dedupliceert niet gewone invoeren. Dat beleid maakt het voorbeeld voorspelbaar zonder stilletjes te herschrijven wat iemand invoerde.
De volgende drie bestanden vormen samen één complete voorbeeld-app. Maak een macOS App-project in Xcode met SwiftUI en een actuele macOS SDK. Vervang het gegenereerde app-startpunt door het derde bestand, en voeg de eerste twee bestanden toe aan hetzelfde target. De code gebruikt de huidige gedocumenteerde token-stijl gevallen, inclusief .none en .rounded. Oudere voorbeelden kunnen andere spellings gebruiken; check de SDK waar je tegen compileert. NSTokenField.TokenStyle.
Definieer het model en vertegenwoordigd object
Plaats dit in Entry.swift. De onveranderlijke LabelToken is het object dat AppKit ontvangt voor een herkend label. Entry is het waardemodel dat aan de SwiftUI-interface wordt geretourneerd.
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
}
Brug het native veld over naar SwiftUI
Plaats dit in MixedEntryField.swift. De representable initialiseert het controle-element eenmaal en houdt de callback actueel tijdens SwiftUI-updates. De coordinator implementeert de AppKit-delegate. Dit volgt Apple's gedocumenteerde creatie-, update- en coordinator-levenscyclus. SwiftUI beheert de layout van de vertegenwoordigde weergave, dus de wrapper stelt de frame niet in. 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)
}
}
}
De conversiemethode retourneert de oorspronkelijke bewerkstring voor een niet-herkende invoer. Apple documenteert expliciet het retourneren van de bewerkstring wanneer er geen aangepast vertegenwoordigd object is. De stijl-delegate overschrijft de veld-brede stijl voor elk object; herkende labels gebruiken .rounded, en strings gebruiken .none. Apple's gearchiveerde architectuurgids beschrijft ook het substitueren van plain-text tokens voor geëncapsuleerde tokens via deze delegate-hook. representedObjectForEditing, styleForRepresentedObject, How Token Fields Work.
Beide string-conversiemethoden zijn belangrijk. De weergavemethode levert een leesbaar label voor een vertegenwoordigd object. De bewerkmethode levert de bewerkbare tekstuele vorm; Apple documenteert het retourneren van nil daar wanneer een token niet bewerkbaar moet zijn. Dit voorbeeld retourneert in plaats daarvan het label. displayStringForRepresentedObject, editingStringForRepresentedObject.
Voeg een kleine interface toe voor het inspecteren van vastgelegde waarden
Plaats dit in MixedTokenDemoApp.swift. Houd slechts één @main app-declaratie in het target. Het tweede tekstveld biedt een expliciete bestemming voor het verplaatsen van toetsenbordfocus uit het gemengde veld.
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)
}
}
Wijzigingen vastleggen zonder actief bewerken te storen
Deze voorbeeld blootst een vastgelegde snapshot in plaats van een binding die bij elke toetsaanslag verandert. Apple definieert controlTextDidEndEditing als de notificatie dat bewerken is voltooid en wijzigingen zijn vastgelegd. Het lezen van de vertegenwoordigde objecten op die grens geeft de omringende interface een duidelijk punt om het resultaat te gebruiken. controlTextDidEndEditing.
Vermijd het toewijzen van objectValue onvoorwaardelijk vanuit updateNSView. Apple documenteert dat het instellen van deze eigenschap terwijl een cel wordt bewerkt, bewerking afbreekt voordat de nieuwe waarde wordt toegewezen. Het lezen ervan tijdens bewerken roept ook validatie aan. Dit zijn gevolgrijke operaties, geen passieve synchronisatie. objectValue.
De wrapper behandelt initialEntries daarom als een initiële zaad. Het veranderen van dat argument na creatie herlaadt het veld niet. Als je applicatie later externe vervanging nodig heeft, ontwerp dan een expliciete vervangoperatie met een beleid voor lopende bewerkingen. Voltooi en accepteer de huidige bewerking, of gooi het bewust weg, voordat je de vertegenwoordigde waarden vervangt.
De decoder vermijdt ook compactMap: een onbekend vertegenwoordigd object produceert een fout in plaats van te verdwijnen uit het resultaat. De interface rapporteert die fout en laat het veld intact. Dat is belangrijk bij het uitbreiden van het voorbeeld met extra objecttypen.
Controleer het gemengde-invoergedrag op je doel-macOS-versies
De code van dit manuscript werd geïnspecteerd tegen de opgehaalde API-declaraties; het werd hier niet gecompileerd of uitgevoerd omdat de beschikbare omgeving Linux is zonder een Swift-toolchain. Bouw en oefen de app op een Mac voordat je het in een grotere interface opneemt.
Gebruik deze diagnostische reeks om herkenning, stijl en vastlegproblemen te onderscheiden:
- Bevestig dat het initiële
Cedar-object een token-weergave heeft. - Voeg
Slatetoe en tik een komma. Bevestig dat het herkende label een token-weergave krijgt. - Voeg
draft notestoe, verplaats dan de focus naar het tweede veld. Bevestig dat de zin plain blijft en als tekst verschijnt in de vastgelegde uitvoer. - Voer lowercase
cedarin. Bevestig dat het tekst blijft onder het exact-match-beleid van het voorbeeld. - Bewerk een bestaand herkend label tot een onbekende zin, voltooi bewerken, en controleer het resulterende type.
- Verwijder elke invoer en voltooi bewerken. Bevestig dat de uitvoer een leeg veld rapporteert.
- Probeer selectie, verwijderen, ongedaan maken, en tekstsamenstelling met de invoermethoden die je wilt ondersteunen. Controleer dat ongerelateerde SwiftUI-updates bewerken niet herstarten.
Een komma is syntaxis in dit voorbeeld: het scheidt invoeren en wordt niet bewaard als letterlijke inhoud. Spaties zijn niet geconfigureerd als scheidingstekens. Breid de scheidingstekenset niet onzinvoer uit als zinnen de toegevoegde karakters moeten bevatten. De scheidingstekenset wordt beheerd via tokenizingCharacterSet. tokenizingCharacterSet.
Gebruik geen herhaalde Return-toetsaanslagen als je enige vastlegtest. Apple's gearchiveerde beschrijving legt uit dat Return de huidige string kan tokenizen of de actie van het controle-element kan sturen. Focus verplaatsen naar een ander bewerkbaar controle-element maakt de eind-bewerkingsgrens van dit voorbeeld makkelijker te inspecteren. Het archief levert gedragsachtergrond; de actuele documentatie levert de Swift-declaraties die hierboven gebruikt worden. How Token Fields Work.
Wanneer moet je TextKit gebruiken?
Kies de rijker tekst-systeempad wanneer het vereiste resultaat continu pros is met inline tokens op willekeurige posities. Bijvoorbeeld, het behouden van de exacte leestekens in Arrange [Cedar], then add a note. vereist een ander bewerkingscontract dan het consumeren van komma's tussen invoeren.
De ondersteunde basis is NSTextView met geattribueerde tekst en NSTextAttachment-objecten. Apple documenteert bijlagen als attribuutwaarden in een geattribueerde string, met een bijlagecel op macOS voor tekenen en muisinteractie. View-based bijlagemogelijkheden zijn additioneel gedocumenteerd voor macOS 12 en later. Deze API's leveren bouwstenen; ze leveren niet het complete tokenmodel, invoerbeleid, of exportformaat voor je app. NSTextView, NSTextAttachment.
Behoud de TextKit-grens wanneer je die route neemt: Apple waarschuwt dat expliciet toegang tot een NSTextView of tekstcontainer's layoutManager op macOS 12 en later het overschakelt naar compatibiliteitsmodus met NSLayoutManager. Beslis welk tekstlayoutsysteem je implementatie gebruikt voordat je voorbeelden combineert. NSTextView.
Voor een compact veld van herkende labels en vrije-tekstinvoeren, begin met het native tokenveld en zijn delegate-conversies. Houd gewone tekst intact, maak scheidingstekens expliciet, en lees vastgelegde vertegenwoordigde objecten. Ga naar een documenteditor wanneer exacte inline-tekststructuur onderdeel wordt van de taak.
Bronnen
- 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 Documentatie Archief
- How Token Fields Work — Apple Documentatie Archief
- NSTextView — Apple
- NSTextAttachment — Apple