Use o NSTokenField do AppKit dentro do SwiftUI, retornando um objeto personalizado para rótulos reconhecidos e uma string para texto comum. Escolha o estilo de exibição para cada objeto, para que os rótulos conhecidos apareçam como tokens e o texto livre permaneça simples.
Este guia cria um editor compacto de rótulos para seu próprio app para Mac. Seu vocabulário sintético contém Cedar e Slate; as pessoas também podem inserir frases como draft notes. O editor informa seu conteúdo quando a edição termina. Este é um campo para entradas mistas, no qual as vírgulas funcionam como separadores — não um editor de frases que preserva todos os sinais de pontuação.
Escolha o controle adequado à tarefa de edição
A Apple descreve o NSTokenField como um campo de texto que converte o texto inserido em tokens visualmente distintos. Seu delegado pode fornecer objetos representados, strings de exibição, strings editáveis e um estilo separado para cada objeto. Esses recursos oferecem um ponto de partida compatível para esta interface. NSTokenField, NSTokenFieldDelegate.
Antes de implementá-lo, faça uma distinção: aceitar entradas arbitrárias e editar um documento arbitrário são tarefas diferentes. Um campo de rótulos separados por vírgulas pode consumir as vírgulas como separadores. Um editor de modelos talvez precise preservar vírgulas, espaços e texto em posições exatas ao redor de objetos incorporados.
| Tarefa de edição do usuário | Ponto de partida adequado |
|---|---|
| Inserir rótulos reconhecidos junto com entradas de texto arbitrárias | NSTokenField com estilos por objeto |
| Inserir uma única string comum | SwiftUI TextField |
| Compor texto contínuo com objetos incorporados e pontuação exata | NSTextView com texto atribuído e anexos |
A documentação do SwiftUI descreve TextField como uma interface editável vinculada a um valor. NSViewRepresentable fornece a ponte documentada para inserir uma visualização do AppKit no SwiftUI. Para edição de documentos mais avançada, NSTextView oferece suporte a texto formatado, anexos, seleção, gerenciamento de entrada e texto marcado. Esses recursos fundamentam as escolhas acima; a tabela é uma recomendação de implementação derivada deles. TextField, NSViewRepresentable, NSTextView.
Mantenha a identidade dos tokens separada do texto comum
Use um modelo ordenado com dois casos: um token reconhecido e texto literal. Isso preserva uma distinção que desapareceria se você reduzisse tudo a uma única string de exibição. Os arrays do Swift preservam a ordem dos elementos; o guia de campos de token da Apple especifica que objectValue contém um array de objetos representados, incluindo strings quando elas são os valores representados. Array, Getting and Setting Token-Field Values.
Neste exemplo, o reconhecimento é deliberadamente exato e diferencia maiúsculas de minúsculas. Cedar se torna um token; cedar permanece como texto. A conversão não remove espaços, não converte para minúsculas, não rejeita nem elimina entradas comuns repetidas. Essa política torna o exemplo previsível sem reescrever silenciosamente o que alguém inseriu.
Os três arquivos a seguir formam um exemplo completo de app. Crie um projeto de app para macOS no Xcode usando SwiftUI e um SDK atual do macOS. Substitua o ponto de entrada gerado do app pelo terceiro arquivo e adicione os dois primeiros arquivos ao mesmo destino. O código usa os casos de estilo de token documentados atualmente, incluindo .none e .rounded. Exemplos mais antigos podem usar grafias diferentes; confira o SDK com o qual você compila. NSTokenField.TokenStyle.
Defina o modelo e o objeto representado
Coloque este código em Entry.swift. O LabelToken imutável é o objeto que o AppKit recebe para um rótulo reconhecido. Entry é o modelo de valor retornado à interface do 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
}
Conecte o campo nativo ao SwiftUI
Coloque este código em MixedEntryField.swift. O representable inicializa o controle uma única vez e mantém o callback atualizado durante as atualizações do SwiftUI. Seu coordenador implementa o delegado do AppKit. Isso segue o ciclo de vida documentado pela Apple para criação, atualização e coordenação. O SwiftUI controla o layout da visualização representada, então o wrapper não define seu frame. 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)
}
}
}
O método de conversão retorna a string de edição original para uma entrada não reconhecida. A Apple documenta explicitamente que se deve retornar a string de edição quando não há um objeto representado personalizado. O delegado de estilo substitui o estilo geral do campo para cada objeto; os rótulos reconhecidos usam .rounded, e as strings usam .none. O guia de arquitetura arquivado da Apple também descreve a substituição de tokens encapsulados por tokens de texto simples por meio desse recurso do delegado. representedObjectForEditing, styleForRepresentedObject, How Token Fields Work.
Os dois métodos de conversão de strings são importantes. O método de exibição fornece um rótulo legível para um objeto representado. O método de edição fornece sua forma textual editável; a Apple documenta que se retorne nil nele quando um token não deve ser editável. Este exemplo retorna o rótulo. displayStringForRepresentedObject, editingStringForRepresentedObject.
Adicione uma pequena interface para inspecionar os valores confirmados
Coloque este código em MixedTokenDemoApp.swift. Mantenha apenas uma declaração @main no destino. O segundo campo de texto oferece um destino explícito para mover o foco do teclado para fora do campo misto.
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)
}
}
Confirme as alterações sem interromper a edição ativa
Este exemplo expõe um instantâneo confirmado, em vez de um vínculo que muda a cada tecla pressionada. A Apple define controlTextDidEndEditing como a notificação de que a edição terminou e as alterações foram confirmadas. Ler os objetos representados nesse momento dá à interface ao redor um ponto claro para usar o resultado. controlTextDidEndEditing.
Evite atribuir objectValue incondicionalmente a partir de updateNSView. A Apple documenta que definir essa propriedade enquanto uma célula está em edição interrompe a edição antes de atribuir o novo valor. Lê-la durante a edição também invoca a validação. São operações com consequências, não uma sincronização passiva. objectValue.
Por isso, o wrapper trata initialEntries como um valor inicial. Alterar esse argumento após a criação não recarrega o campo. Se seu app precisar posteriormente de substituições externas, crie uma operação explícita de substituição com uma política para edições pendentes. Finalize e aceite a edição atual ou descarte-a deliberadamente antes de substituir os valores representados.
O decodificador também evita compactMap: um objeto representado desconhecido gera uma falha, em vez de desaparecer do resultado. A interface informa essa falha e mantém o campo intacto. Isso é importante ao estender o exemplo com outros tipos de objeto.
Verifique o comportamento de entradas mistas nas versões do macOS que você pretende usar
O código deste manuscrito foi conferido com base nas declarações de API consultadas; não foi compilado nem executado aqui porque o ambiente disponível é Linux e não tem um toolchain do Swift. Compile e teste o app em um Mac antes de incorporá-lo a uma interface maior.
Use esta sequência de diagnóstico para distinguir problemas de reconhecimento, estilo e confirmação:
- Confirme que o objeto inicial
Cedartem aparência de token. - Adicione
Slatee digite uma vírgula. Confirme que o rótulo reconhecido recebe aparência de token. - Adicione
draft notese mova o foco para o segundo campo. Confirme que a frase permanece simples e aparece como texto na saída confirmada. - Insira
cedarem minúsculas. Confirme que permanece como texto segundo a política de correspondência exata do exemplo. - Edite um rótulo reconhecido existente, transformando-o em uma frase desconhecida, finalize a edição e confira o tipo resultante.
- Remova todas as entradas e finalize a edição. Confirme que a saída informa que o campo está vazio.
- Teste seleção, exclusão, desfazer e composição de texto com os métodos de entrada que você pretende oferecer. Confira se atualizações não relacionadas do SwiftUI não reiniciam a edição.
Neste exemplo, a vírgula é sintaxe: ela separa as entradas e não é preservada como conteúdo literal. Os espaços não são configurados como separadores. Não amplie casualmente o conjunto de delimitadores se as frases precisarem conter os caracteres adicionados. O conjunto de delimitadores é controlado por tokenizingCharacterSet. tokenizingCharacterSet.
Não use várias pressões de Return como seu único teste de confirmação. A descrição arquivada da Apple explica que Return pode tokenizar a string atual ou acionar a ação do controle. Mover o foco para outro controle editável facilita a inspeção do limite de fim de edição deste exemplo. O arquivo fornece contexto sobre o comportamento; a documentação atual fornece as declarações do Swift usadas acima. How Token Fields Work.
Quando você deve usar o TextKit?
Escolha o caminho do sistema de texto mais avançado quando o resultado necessário for prosa contínua com tokens incorporados em posições arbitrárias. Por exemplo, preservar a pontuação exata em Arrange [Cedar], then add a note. exige um contrato de edição diferente do que consumir vírgulas entre entradas.
A base compatível é NSTextView com texto atribuído e objetos NSTextAttachment. A Apple documenta anexos como valores de atributos em uma string atribuída, com uma célula de anexo no macOS para desenho e interação com o mouse. Os recursos de anexos baseados em visualizações também estão documentados para macOS 12 e versões posteriores. Essas APIs fornecem blocos de construção; elas não fornecem o modelo completo de tokens, a política de inserção nem o formato de exportação do seu app. NSTextView, NSTextAttachment.
Preserve os limites do TextKit ao seguir esse caminho: a Apple alerta que acessar explicitamente o layoutManager de um NSTextView ou contêiner de texto no macOS 12 e versões posteriores o alterna para o modo de compatibilidade usando NSLayoutManager. Decida qual sistema de layout de texto sua implementação usará antes de combinar exemplos. NSTextView.
Para um campo compacto de rótulos reconhecidos e entradas de texto livre, comece pelo campo de tokens nativo e suas conversões de delegado. Mantenha o texto comum intacto, torne os separadores explícitos e leia os objetos representados confirmados. Passe para um editor de documentos quando a estrutura exata do texto embutido fizer parte da tarefa.
Fontes
- 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