Użyj AppKitowego NSTokenField wewnątrz SwiftUI, zwracając niestandardowy obiekt dla rozpoznanych etykiet i ciąg znaków dla zwykłego tekstu. Wybierz styl wyświetlania dla każdego obiektu, aby znane etykiety pojawiały się jako tokeny, a zwykły tekst pozostawał bez zmian.
Ten przewodnik pokazuje, jak zbudować kompaktowy edytor etykiet we własnej aplikacji na Maca. Jego przykładowy słownik zawiera Cedar i Slate; można też wpisywać frazy, takie jak draft notes. Edytor przekazuje swoją zawartość po zakończeniu edycji. To pole do wprowadzania mieszanych wpisów, w którym przecinki pełnią rolę separatorów — nie edytor zdań zachowujący każdy znak interpunkcyjny.
Wybierz kontrolkę odpowiednią do zadania edycji
Apple opisuje NSTokenField jako pole tekstowe, które przekształca wpisany tekst w wizualnie odrębne tokeny. Jego delegat może udostępniać reprezentowane obiekty, ciągi wyświetlane i edytowalne oraz osobny styl dla każdego obiektu. Te mechanizmy stanowią obsługiwaną podstawę tego interfejsu. NSTokenField, NSTokenFieldDelegate.
Przed implementacją rozróżnij dwa zadania: akceptowanie dowolnych wpisów i edytowanie dowolnego dokumentu to nie to samo. Pole etykiet oddzielonych przecinkami może traktować przecinki jako separatory. Edytor szablonów może wymagać zachowania przecinków, spacji i tekstu w dokładnie określonych miejscach wokół obiektów osadzonych w tekście.
| Zadanie edycji użytkownika | Odpowiedni punkt wyjścia |
|---|---|
| Wpisywanie rozpoznanych etykiet oraz dowolnych wpisów tekstowych | NSTokenField ze stylami przypisanymi do poszczególnych obiektów |
| Wpisywanie pojedynczego zwykłego ciągu | SwiftUI TextField |
| Tworzenie ciągłego tekstu z obiektami osadzonymi w dowolnych miejscach i dokładną interpunkcją | NSTextView z tekstem atrybutowanym i załącznikami |
SwiftUI opisuje TextField jako edytowalny interfejs powiązany z wartością. NSViewRepresentable udostępnia udokumentowany sposób osadzania widoku AppKit w SwiftUI. Do bardziej rozbudowanej edycji dokumentów NSTextView obsługuje tekst sformatowany, załączniki, zaznaczanie, zarządzanie wprowadzaniem i tekst oznaczony. Te możliwości uzasadniają powyższe wybory; tabela zawiera zalecenia implementacyjne oparte na tych funkcjach. TextField, NSViewRepresentable, NSTextView.
Oddziel tożsamość tokenu od zwykłego tekstu
Użyj uporządkowanego modelu z dwoma przypadkami: rozpoznanym tokenem i tekstem dosłownym. Dzięki temu zachowasz rozróżnienie, które zniknęłoby po spłaszczeniu wszystkiego do jednego ciągu wyświetlanego. Tablice w Swift zachowują kolejność elementów; przewodnik Apple dotyczący pól tokenów określa, że objectValue zawiera tablicę reprezentowanych obiektów, w tym ciągów, gdy reprezentowanymi wartościami są ciągi. Array, Pobieranie i ustawianie wartości pól tokenów.
W tym przykładzie rozpoznawanie jest celowo dokładne i uwzględnia wielkość liter. Cedar staje się tokenem; cedar pozostaje tekstem. Konwersja nie przycina, nie zamienia na małe litery, nie odrzuca ani nie usuwa duplikatów zwykłych wpisów. Dzięki temu przykład działa przewidywalnie i nie zmienia po cichu tego, co wpisano.
Poniższe trzy pliki tworzą kompletną przykładową aplikację. W Xcode utwórz projekt aplikacji macOS używający SwiftUI i aktualnego SDK macOS. Zastąp wygenerowany punkt wejścia aplikacji trzecim plikiem i dodaj dwa pierwsze pliki do tego samego celu kompilacji. Kod używa aktualnie udokumentowanych przypadków stylów tokenów, w tym .none i .rounded. Starsze przykłady mogą używać innych nazw; sprawdź SDK, z którym kompilujesz. NSTokenField.TokenStyle.
Zdefiniuj model i reprezentowany obiekt
Umieść ten kod w pliku Entry.swift. Niezmienny LabelToken to obiekt, który AppKit otrzymuje dla rozpoznanej etykiety. Entry to model wartości przekazywany do interfejsu 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
}
Osadź natywne pole w SwiftUI
Umieść ten kod w pliku MixedEntryField.swift. Reprezentowalny widok inicjuje kontrolkę raz i aktualizuje wywołanie zwrotne podczas aktualizacji SwiftUI. Jego koordynator implementuje delegata AppKit. Rozwiązanie to jest zgodne z udokumentowanym przez Apple cyklem życia tworzenia, aktualizowania i koordynatora. SwiftUI zarządza układem reprezentowanego widoku, więc opakowujący go komponent nie ustawia jego ramki. 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)
}
}
}
Metoda konwersji zwraca oryginalny ciąg edycji dla nierozpoznanego wpisu. Apple wyraźnie dokumentuje zwracanie ciągu edycji, gdy nie ma niestandardowego reprezentowanego obiektu. Delegat stylu nadpisuje ogólny styl pola dla każdego obiektu; rozpoznane etykiety używają .rounded, a ciągi — .none. Zarchiwizowany przewodnik architektury Apple opisuje również zastępowanie tokenów hermetyzowanych tokenami zwykłego tekstu za pomocą tego punktu rozszerzeń delegata. representedObjectForEditing, styleForRepresentedObject, Jak działają pola tokenów.
Obie metody konwersji ciągów mają znaczenie. Metoda wyświetlania udostępnia czytelną etykietę reprezentowanego obiektu. Metoda edycji udostępnia jego edytowalną postać tekstową; Apple dokumentuje zwracanie tam nil, gdy token nie powinien być edytowalny. W tym przykładzie zwracana jest etykieta. displayStringForRepresentedObject, editingStringForRepresentedObject.
Dodaj prosty interfejs do sprawdzania zatwierdzonych wartości
Umieść ten kod w pliku MixedTokenDemoApp.swift. W celu kompilacji pozostaw tylko jedną deklarację aplikacji @main. Drugie pole tekstowe stanowi jawny cel, do którego można przenieść fokus klawiatury z pola mieszanych wpisów.
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)
}
}
Zatwierdzaj zmiany bez zakłócania aktywnej edycji
Ten przykład udostępnia zatwierdzony stan zamiast powiązania aktualizowanego przy każdym naciśnięciu klawisza. Apple definiuje controlTextDidEndEditing jako powiadomienie o zakończeniu edycji i zatwierdzeniu zmian. Odczyt reprezentowanych obiektów w tym momencie daje otaczającemu interfejsowi jasny punkt, w którym może użyć wyniku. controlTextDidEndEditing.
Unikaj bezwarunkowego przypisywania objectValue z poziomu updateNSView. Apple dokumentuje, że ustawienie tej właściwości podczas edytowania komórki przerywa edycję przed przypisaniem nowej wartości. Jej odczyt podczas edycji również wywołuje walidację. To działania o istotnych konsekwencjach, a nie bierna synchronizacja. objectValue.
Dlatego opakowujący komponent traktuje initialEntries jako wartość początkową. Zmiana tego argumentu po utworzeniu pola nie powoduje jego ponownego wczytania. Jeśli aplikacja będzie później wymagać zewnętrznego zastępowania wartości, zaprojektuj jawną operację zastępowania z zasadami obsługi oczekujących zmian. Przed zastąpieniem reprezentowanych wartości zakończ i zaakceptuj bieżącą edycję albo celowo ją odrzuć.
Dekoder unika też użycia compactMap: nieznany reprezentowany obiekt powoduje błąd zamiast zniknąć z wyniku. Interfejs zgłasza ten błąd i pozostawia pole bez zmian. Ma to znaczenie przy rozszerzaniu przykładu o kolejne typy obiektów.
Sprawdź obsługę mieszanych wpisów w docelowych wersjach macOS
Kod w tym artykule sprawdzono względem pobranych deklaracji API; nie został tutaj skompilowany ani uruchomiony, ponieważ dostępne środowisko to Linux bez toolchaina Swift. Przed włączeniem go do większego interfejsu zbuduj i przetestuj aplikację na Macu.
Użyj tej sekwencji diagnostycznej, aby rozróżnić problemy z rozpoznawaniem, stylem i zatwierdzaniem:
- Sprawdź, czy początkowy obiekt
Cedarma wygląd tokenu. - Dodaj
Slatei wpisz przecinek. Sprawdź, czy rozpoznana etykieta otrzymuje wygląd tokenu. - Dodaj
draft notes, a następnie przenieś fokus do drugiego pola. Sprawdź, czy fraza pozostaje zwykłym tekstem i pojawia się jako tekst w zatwierdzonym wyniku. - Wpisz małymi literami
cedar. Sprawdź, czy zgodnie z zasadą dokładnego dopasowania z przykładu wpis pozostaje tekstem. - Zmień istniejącą rozpoznaną etykietę na nieznaną frazę, zakończ edycję i sprawdź typ otrzymanego wpisu.
- Usuń wszystkie wpisy i zakończ edycję. Sprawdź, czy wynik wskazuje puste pole.
- Przetestuj zaznaczanie, usuwanie, cofanie i wpisywanie tekstu za pomocą metod wprowadzania, które zamierzasz obsługiwać. Sprawdź, czy niezwiązane aktualizacje SwiftUI nie przerywają edycji.
W tym przykładzie przecinek jest składnią: oddziela wpisy i nie jest zachowywany jako dosłowna zawartość. Spacje nie są skonfigurowane jako separatory. Nie dodawaj pochopnie kolejnych separatorów, jeśli frazy muszą zawierać dodane znaki. Zestaw separatorów jest określany przez tokenizingCharacterSet. tokenizingCharacterSet.
Nie używaj wielokrotnego naciskania klawisza Return jako jedynego testu zatwierdzania. Zarchiwizowany opis Apple wyjaśnia, że Return może przekształcić bieżący ciąg w token lub wywołać akcję kontrolki. Przeniesienie fokusu do innej edytowalnej kontrolki ułatwia sprawdzenie granicy zakończenia edycji w tym przykładzie. Archiwum dostarcza kontekstu dotyczącego zachowania; deklaracje Swift użyte powyżej pochodzą z aktualnej dokumentacji. Jak działają pola tokenów.
Kiedy zamiast tego użyć TextKit?
Wybierz bardziej rozbudowaną ścieżkę systemu tekstowego, gdy wymagany wynik to ciągła proza z tokenami osadzonymi w dowolnych miejscach. Na przykład zachowanie dokładnej interpunkcji w Arrange [Cedar], then add a note. wymaga innego modelu edycji niż usuwanie przecinków rozdzielających wpisy.
Obsługiwaną podstawą jest NSTextView z tekstem atrybutowanym i obiektami NSTextAttachment. Apple dokumentuje załączniki jako wartości atrybutów w ciągu atrybutowanym, z komórką załącznika w macOS do rysowania i obsługi interakcji myszy. Obsługa załączników opartych na widokach jest dodatkowo udokumentowana dla macOS 12 i nowszych. Te API dostarczają elementów składowych; nie zapewniają kompletnego modelu tokenów, zasad wstawiania ani formatu eksportu dla aplikacji. NSTextView, NSTextAttachment.
Jeśli wybierzesz tę drogę, zachowaj granicę TextKit: Apple ostrzega, że jawny dostęp do layoutManager obiektu NSTextView lub kontenera tekstu w macOS 12 i nowszych przełącza go w tryb zgodności używający NSLayoutManager. Zdecyduj, którego systemu układu tekstu użyje implementacja, zanim połączysz przykłady. NSTextView.
W przypadku kompaktowego pola z rozpoznanymi etykietami i wpisami tekstowymi zacznij od natywnego pola tokenów i konwersji delegata. Zachowaj zwykły tekst, jawnie określ separatory i odczytuj zatwierdzone reprezentowane obiekty. Przejdź do edytora dokumentów, gdy dokładna struktura tekstu wbudowanego stanie się częścią zadania.
Źródła
- 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
- Pobieranie i ustawianie wartości pól tokenów — Archiwum dokumentacji Apple
- Jak działają pola tokenów — Archiwum dokumentacji Apple
- NSTextView — Apple
- NSTextAttachment — Apple