Använd App Kits NSTokenField i SwiftUI. Fältet returnerar ett anpassat objekt för igenkända etiketter och en sträng för vanlig text. Välj visningsstil för varje objekt så att kända etiketter visas som token medan fri text förblir vanlig text.

Den här guiden bygger en kompakt etikettredigerare i din egen Mac-app. Dess syntetiska vokabulär innehåller Cedar och Slate; användare kan också ange fraser som draft notes. Redigeraren rapporterar innehållet när redigeringen avslutas. Det här är ett fält för blandade poster där kommatecken fungerar som avgränsare – inte en meningsredigerare som bevarar varje skiljetecken.

Välj kontrollen som passar redigeringsuppgiften

Apple beskriver NSTokenField som ett textfält som omvandlar inskriven text till visuellt åtskilda token. Dess delegat kan tillhandahålla representerade objekt, visningssträngar, redigerbara strängar och en separat stil för varje objekt. Dessa kopplingspunkter är en understödd utgångspunkt för det här gränssnittet. NSTokenField, NSTokenFieldDelegate.

Gör en åtskillnad innan du implementerar det: att acceptera godtyckliga poster och att redigera ett godtyckligt dokument är två olika uppgifter. Ett kommaseparerat etikettfält kan använda kommatecken som avgränsare. En malleditor kan behöva behålla kommatecken, blanksteg och text på exakta positioner runt infogade objekt.

Läsarens redigeringsuppgift Lämplig utgångspunkt
Ange igenkända etiketter tillsammans med godtyckliga textposter NSTokenField med stilar per objekt
Ange en vanlig sträng SwiftUI TextField
Skapa löpande text med infogade objekt och exakt interpunktion NSTextView med attributtext och bilagor

SwiftUI dokumenterar TextField som ett redigerbart gränssnitt bundet till ett värde. NSViewRepresentable tillhandahåller den dokumenterade bryggan för att placera en AppKit-vy i SwiftUI. För mer avancerad dokumentredigering stöder NSTextView formaterad text, bilagor, markering, inmatningshantering och markerad text. Dessa funktioner ligger till grund för valen ovan; tabellen är en implementeringsrekommendation som härletts från dem. TextField, NSViewRepresentable, NSTextView.

Håll tokenidentitet åtskild från vanlig text

Använd en ordnad modell med två fall: en igenkänd token och bokstavlig text. Då bevaras en åtskillnad som skulle gå förlorad om allt plattades till en enda visningssträng. Swift-arrayer bevarar elementens ordning; Apples guide för tokenfält anger att objectValue innehåller en array av representerade objekt, inklusive strängar när strängar är de representerade värdena. Array, Hämta och ange tokenfältsvärden.

I det här exemplet är igenkänningen avsiktligt exakt och skiftlägeskänslig. Cedar blir en token; cedar förblir text. Konverteringen trimmar inte, gör inte texten till gemener, avvisar inte och tar inte bort dubbletter bland vanliga poster. Den här policyn gör exemplet förutsägbart utan att i tysthet skriva om det någon har angett.

Följande tre filer utgör en komplett exempelapp. Skapa ett macOS App-projekt i Xcode med SwiftUI och ett aktuellt macOS-SDK. Ersätt den genererade appens startpunkt med den tredje filen och lägg till de två första filerna i samma mål. Koden använder de aktuella dokumenterade tokenstilsfallen, däribland .none och .rounded. Äldre exempel kan använda andra stavningar; kontrollera SDK:t som du kompilerar mot. NSTokenField.TokenStyle.

Definiera modellen och det representerade objektet

Lägg detta i Entry.swift. Den oföränderliga LabelToken är objektet som AppKit tar emot för en igenkänd etikett. Entry är värdemodellen som returneras till SwiftUI-gränssnittet.

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
}

Brygga det inbyggda fältet till SwiftUI

Lägg detta i MixedEntryField.swift. Representable-typen initierar kontrollen en gång och håller återanropet aktuellt vid uppdateringar i SwiftUI. Dess koordinator implementerar AppKit-delegaten. Detta följer Apples dokumenterade livscykel för skapande, uppdatering och koordinator. SwiftUI styr layouten för den representerade vyn, så omslaget anger inte dess ram. 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)
        }
    }
}

Konverteringsmetoden returnerar den ursprungliga redigeringssträngen för en post som inte känns igen. Apple dokumenterar uttryckligen att redigeringssträngen ska returneras när det inte finns något anpassat representerat objekt. Stildelegaten åsidosätter fältets övergripande stil för varje objekt; igenkända etiketter använder .rounded, och strängar använder .none. Apples arkiverade arkitekturbeskrivning förklarar också hur kapslade token kan ersättas med token i oformaterad text via den här delegatkopplingen. representedObjectForEditing, styleForRepresentedObject, Så fungerar tokenfält.

Båda strängkonverteringsmetoderna spelar roll. Visningsmetoden tillhandahåller en läsbar etikett för ett representerat objekt. Redigeringsmetoden tillhandahåller dess redigerbara textform; Apple dokumenterar att nil ska returneras där när en token inte ska kunna redigeras. Det här exemplet returnerar i stället etiketten. displayStringForRepresentedObject, editingStringForRepresentedObject.

Lägg till ett enkelt gränssnitt för att granska bekräftade värden

Lägg detta i MixedTokenDemoApp.swift. Ha bara en @main-appdeklaration i målet. Det andra textfältet är en tydlig destination för att flytta tangentbordsfokus ut ur det blandade fältet.

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)
    }
}

Bekräfta ändringar utan att störa aktiv redigering

Det här exemplet visar en bekräftad ögonblicksbild i stället för en bindning som ändras vid varje tangenttryckning. Apple definierar controlTextDidEndEditing som aviseringen om att redigeringen har avslutats och ändringarna har bekräftats. Om de representerade objekten läses av vid den gränsen får det omgivande gränssnittet en tydlig tidpunkt då resultatet kan användas. controlTextDidEndEditing.

Undvik att ovillkorligt tilldela objectValue från updateNSView. Apple dokumenterar att redigeringen avbryts innan det nya värdet tilldelas om den här egenskapen anges medan en cell redigeras. Om egenskapen läses under redigering anropas också validering. Det är betydelsefulla åtgärder, inte passiv synkronisering. objectValue.

Omslaget behandlar därför initialEntries som ett initialt startvärde. Om det argumentet ändras efter skapandet laddas fältet inte om. Om din app senare behöver externa ersättningar bör du utforma en uttrycklig ersättningsåtgärd med en policy för väntande redigeringar. Slutför och godkänn den aktuella redigeringen, eller kasta avsiktligt bort den, innan de representerade värdena ersätts.

Avkodaren undviker också compactMap: ett okänt representerat objekt ger ett fel i stället för att försvinna från resultatet. Gränssnittet rapporterar felet och lämnar fältet intakt. Det här spelar roll när exemplet utökas med fler objekttyper.

Verifiera beteendet för blandade poster på dina målversioner av macOS

Koden i det här manuskriptet granskades mot de hämtade API-deklarationerna; den kompilerades eller kördes inte här eftersom den tillgängliga miljön är Linux utan en Swift-verktygskedja. Bygg och prova appen på en Mac innan du införlivar den i ett större gränssnitt.

Använd den här felsökningssekvensen för att skilja mellan problem med igenkänning, stil och bekräftelse:

  1. Kontrollera att det ursprungliga Cedar-objektet ser ut som en token.
  2. Lägg till Slate och skriv ett kommatecken. Kontrollera att den igenkända etiketten får tokenutseende.
  3. Lägg till draft notes och flytta sedan fokus till det andra fältet. Kontrollera att frasen förblir vanlig text och visas som text i den bekräftade utdata.
  4. Skriv cedar med små bokstäver. Kontrollera att den förblir text enligt exemplets policy för exakt matchning.
  5. Ändra en befintlig igenkänd etikett till en okänd fras, avsluta redigeringen och kontrollera dess resulterande typ.
  6. Ta bort alla poster och avsluta redigeringen. Kontrollera att utdata visar ett tomt fält.
  7. Prova markering, borttagning, ångring och textkomposition med de inmatningsmetoder du avser att stödja. Kontrollera att orelaterade SwiftUI-uppdateringar inte startar om redigeringen.

Ett kommatecken är syntax i det här exemplet: det skiljer poster åt och bevaras inte som bokstavligt innehåll. Blanksteg är inte konfigurerade som avgränsare. Utöka inte avgränsaruppsättningen lättvindigt om fraser måste kunna innehålla de tillagda tecknen. Avgränsaruppsättningen styrs med tokenizingCharacterSet. tokenizingCharacterSet.

Använd inte upprepade tryck på returtangenten som ditt enda test av bekräftelsen. Apples arkiverade beskrivning förklarar att Retur kan tokenisera den aktuella strängen eller skicka kontrollens åtgärd. Om fokus flyttas till en annan redigerbar kontroll blir det enklare att granska det här exemplets gräns för avslutad redigering. Arkivet ger bakgrund om beteendet; den aktuella dokumentationen ger Swift-deklarationerna som används ovan. Så fungerar tokenfält.

När bör du använda TextKit i stället?

Välj den mer avancerade textsystemvägen när det önskade resultatet är löpande prosa med infogade token på godtyckliga positioner. Att exempelvis bevara den exakta interpunktionen i Arrange [Cedar], then add a note. kräver ett annat redigeringskontrakt än att använda kommatecken som avgränsare mellan poster.

Den understödda grunden är NSTextView med attributtext och NSTextAttachment-objekt. Apple dokumenterar bilagor som attributvärden i en attributsträng, med en bilagecell på macOS för ritning och musinteraktion. Stöd för vybaserade bilagor dokumenteras dessutom för macOS 12 och senare. Dessa API:er tillhandahåller byggstenar; de ger inte din app en komplett tokenmodell, infogningspolicy eller exportformat. NSTextView, NSTextAttachment.

Bevara gränsen för TextKit om du väljer den vägen: Apple varnar för att uttrycklig åtkomst till layoutManager för en NSTextView eller textbehållare på macOS 12 och senare växlar den till kompatibilitetsläge med NSLayoutManager. Bestäm vilket textlayoutsystem din implementation ska använda innan du kombinerar exempel. NSTextView.

För ett kompakt fält med igenkända etiketter och poster med fri text kan du börja med det inbyggda tokenfältet och dess delegatkonverteringar. Bevara vanlig text, gör avgränsare tydliga och läs av de bekräftade representerade objekten. Byt till en dokumentredigerare när exakt struktur för infogad text blir en del av uppgiften.

Källor