Atribua um NSTextFieldDelegate, implemente controlTextDidEndEditing(_:) e copie o stringValue do campo para uma vinculação do SwiftUI. Um coordenador conecta esse retorno de chamada ao estado da visualização, permitindo que um rótulo seja atualizado quando a edição terminar.

Este tutorial cria um pequeno app para Mac com dois rótulos editáveis e um resumo que mostra os valores confirmados. A tarefa é manter o resumo inalterado enquanto alguém digita e, em seguida, atualizá-lo quando o AppKit encerrar a sessão de edição. A Apple documenta que o retorno de chamada de fim da edição ocorre depois que o controle confirma as alterações. Documentação de controlTextDidEndEditing(_:) da Apple

Qual evento deve atualizar a visualização?

Para um NSTextField existente, use o retorno de chamada delegado de edição do controle. O delegado recebe uma notificação cujo object é o controle que a enviou, permitindo que o manipulador identifique o campo e leia seu valor. Documentação de textDidEndEditingNotification da Apple

Escolha o evento de acordo com o comportamento de interface necessário:

Comportamento desejado Abordagem apropriada
Atualizar um valor confirmado quando um campo de texto do AppKit terminar de editar Implemente controlTextDidEndEditing(_:) no delegado do campo.
Acionar um manipulador existente do AppKit com target-action ao concluir a edição Configure a propriedade sendsActionOnEndEditing da célula.
Tratar o envio de um TextField nativo do SwiftUI Anexe .onSubmit para o comportamento de envio documentado.

Essas abordagens têm contratos diferentes. A Apple documenta que ativar sendsActionOnEndEditing envia a ação depois de pressionar Return, sair do campo com Tab ou clicar em outro campo de texto; desativá-lo restringe a ação à tecla Return. A documentação do SwiftUI especifica que .onSubmit em um TextField responde à tecla Return. Portanto, isso não estabelece um retorno de chamada geral para perda de foco em um campo AppKit incorporado. Documentação de sendsActionOnEndEditing da Apple, documentação de onSubmit(of:_:) da Apple

Neste app, o delegado é o único lugar que confirma o texto de um campo. Se você também adicionar um manipulador target-action depois, evite executar o mesmo efeito de confirmação nos dois manipuladores.

Crie um exemplo completo com SwiftUI e AppKit

Crie no Xcode um projeto de app para macOS usando Swift e a interface SwiftUI. Use uma cadeia de ferramentas recente compatível com Swift 6 e defina macOS 13 ou posterior como destino de implantação para este exemplo. Substitua o arquivo de entrada gerado do app pelo programa completo abaixo e remova o ContentView.swift gerado se ele declarar outro ContentView. Mantenha exatamente um ponto de entrada @main. A documentação de App da Apple descreve essa estrutura de ponto de entrada. Documentação de App da Apple

O destino macOS 13 do exemplo é uma escolha de projeto, não a versão em que essa integração foi introduzida: a Apple lista NSViewRepresentable como disponível a partir do macOS 10.15. O AppKit torna esta implementação específica para um destino macOS. Documentação de NSViewRepresentable da Apple

import SwiftUI
import AppKit

@main
@MainActor
struct EditingDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

@MainActor
struct ContentView: View {
    @State private var cardTitle = "Blue Card"
    @State private var shelfLabel = "Shelf A"

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Card title")
            CommitTextField(value: $cardTitle, label: "Card title")
                .frame(height: 24)

            Text("Shelf label")
            CommitTextField(value: $shelfLabel, label: "Shelf label")
                .frame(height: 24)

            Divider()

            Text("Committed values")
                .font(.headline)
            Text("Card: \(cardTitle)")
            Text("Shelf: \(shelfLabel)")
        }
        .padding(20)
        .frame(minWidth: 420)
    }
}

@MainActor
struct CommitTextField: NSViewRepresentable {
    @Binding var value: String
    let label: String

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }

    func makeNSView(context: Context) -> NSTextField {
        let field = NSTextField(string: value)
        field.placeholderString = label
        field.delegate = context.coordinator
        return field
    }

    func updateNSView(_ field: NSTextField, context: Context) {
        context.coordinator.parent = self
        field.placeholderString = label
        field.isEnabled = context.environment.isEnabled

        // Leave the active editing buffer alone.
        if field.currentEditor() == nil, field.stringValue != value {
            field.stringValue = value
        }
    }

    static func dismantleNSView(
        _ field: NSTextField,
        coordinator: Coordinator
    ) {
        field.delegate = nil
    }

    @MainActor
    final class Coordinator: NSObject, NSTextFieldDelegate {
        var parent: CommitTextField

        init(parent: CommitTextField) {
            self.parent = parent
            super.init()
        }

        func controlTextDidEndEditing(_ notification: Notification) {
            guard let field = notification.object as? NSTextField else {
                return
            }

            let committedValue = field.stringValue
            if parent.value != committedValue {
                parent.value = committedValue
            }
        }
    }
}

Este é um exemplo completo de app com valores locais sintéticos. Ele foi analisado com base na documentação de API recuperada, mas não foi compilado nem executado aqui: o ambiente de execução disponível é Linux e não tem cadeia de ferramentas Swift nem SDK do macOS. Compile e teste-o no Xcode em um Mac.

Como o valor chega ao resumo

Cada valor @State do componente pai fornece uma vinculação ao wrapper do campo. O coordenador grava por meio dessa vinculação quando a edição termina. Em seguida, o SwiftUI atualiza o resumo porque ele lê o estado alterado. A Apple documenta tanto a atualização de visualizações orientada por estado quanto o uso de vinculações para permitir que visualizações filhas modifiquem o estado mantido pelo componente pai. Documentação de State da Apple

makeNSView cria o controle editável do AppKit e atribui o coordenador como delegado. updateNSView trata das alterações que fluem do SwiftUI para esse controle. O retorno de chamada do delegado trata do fluxo inverso. A orientação de integração da Apple usa essa mesma combinação de coordenador e vinculação para hospedar visualizações do AppKit. Sessão “Use SwiftUI with AppKit” da Apple

A atribuição context.coordinator.parent = self mantém atualizada a representação armazenada pelo coordenador. Um coordenador dura o mesmo tempo que a visualização hospedada, enquanto os valores da representação podem mudar. A Apple demonstra explicitamente a atualização desse valor armazenado durante updateNSView. Explicação da Apple sobre o ciclo de vida do coordenador

Preserve o rascunho durante atualizações do SwiftUI

A condição essencial é field.currentEditor() == nil. Este exemplo mantém deliberadamente o texto ainda não concluído no AppKit, enquanto o SwiftUI conserva o último valor confirmado. Sem uma verificação de edição em andamento, copiar o valor confirmado para o campo durante uma atualização do SwiftUI poderia substituir o rascunho incompleto.

A Apple documenta que currentEditor() retorna o editor de campo do controle ou nil quando ele não tem um. O editor de campo é um NSTextView compartilhado entre os controles da janela. Por isso, verificar apenas se o firstResponder da janela é igual ao NSTextField não substitui adequadamente a consulta ao controle sobre seu editor. Documentação de currentEditor() da Apple

O comportamento resultante é uma política de projeto explícita:

Situação Comportamento do exemplo
Um campo está inativo e sua vinculação muda Copie a vinculação para o campo na próxima atualização da representação.
Um campo tem um editor ativo Preserve o buffer de edição.
A edição termina com texto diferente Grave o texto concluído na vinculação.
A edição termina sem alteração no texto Ignore a atribuição de estado redundante.

Essa política dá prioridade ao rascunho local durante uma sessão de edição. Se outro código do app alterar a mesma vinculação enquanto a pessoa estiver editando, uma confirmação posterior poderá substituir essa alteração. A demonstração não tem outro componente gravando o valor. Em um editor maior, decida como resolver alterações simultâneas — por exemplo, encerre a sessão atual antes de mudar o item selecionado ou mantenha valores separados para rascunho e confirmação e ofereça uma escolha explícita. Não descarte texto incompleto apenas para sincronizar uma nova renderização.

Esta tarefa não exige nenhuma personalização de layout do TextKit. O limite relevante é a sessão de edição do controle e seu editor de campo. O guia de arquitetura de texto da Apple explica a relação entre NSTextField, seu delegado e o NSTextView compartilhado que atua como editor de campo; esse guia está arquivado, enquanto a referência atual de currentEditor() confirma o modelo de editor compartilhado. Guia “Text Editing” da Apple, referência atual da Apple ao editor de campo

Verifique o comportamento de conclusão no app

Use a sequência a seguir para verificar o exemplo depois de compilá-lo. São verificações esperadas, derivadas da implementação e do comportamento de edição documentado, não resultados de execução relatados.

  1. Substitua Blue Card por Green Card. Durante a digitação, o resumo ainda deve mostrar Card: Blue Card.
  2. Pressione Return. O resumo deve mudar para Card: Green Card.
  3. Edite o título do cartão novamente e clique no campo Shelf label. O título concluído do cartão deve aparecer no resumo.
  4. Edite o valor da prateleira e pressione Tab. Confirme que o resumo da prateleira é atualizado e inspecione a navegação pelo teclado.
  5. Confirme um valor vazio. O resumo correspondente deve mostrar o prefixo sem valor; o exemplo aceita strings vazias.
  6. Encerre uma edição sem alterar o texto. A verificação de igualdade deve evitar outra atribuição de estado.

A tecla Return pode concluir a edição sem produzir o movimento de foco que você espera. A Apple documenta que, depois da notificação de fim da edição, o NSTextField tenta executar sua ação e outros tratamentos de Return, e pode selecionar o texto. Tab e Shift-Tab tentam navegar para a próxima ou a anterior visualização de teclado. Ao verificar a interface, trate a conclusão e o destino do foco como comportamentos distintos. Documentação de textDidEndEditing(_:) da Apple

Diagnostique um retorno de chamada que não chega

Primeiro, verifique se field.delegate ainda aponta para o coordenador. A propriedade é fraca, então um objeto delegado temporário sem um proprietário separado não permanecerá disponível. O coordenador da representação fornece aqui o tempo de vida necessário. Documentação de delegate da Apple, explicação da Apple sobre o ciclo de vida do coordenador

Em seguida, confirme que a edição realmente terminou. Um clique em um plano de fundo qualquer não é um teste confiável dessa condição. Use Return ou outro campo editável nesta demonstração e, se o resultado for diferente, inspecione a transição específica do responder.

Se o seu app adicionar validação, verifique control(_:textShouldEndEditing:). O valor de retorno documentado determina se a edição pode terminar. Uma tentativa rejeitada de sair do campo não deve ser tratada como uma confirmação bem-sucedida. Documentação de control(_:textShouldEndEditing:) da Apple

Inspecione também os caminhos de cancelamento. A Apple afirma explicitamente que chamar abortEditing() para descartar edições pendentes não invoca controlTextDidEndEditing(_:). Portanto, uma operação de cancelamento precisa de seu próprio comportamento de estado deliberado. Documentação da Apple sobre o retorno de chamada de fim da edição

Solicite a conclusão por meio de um comando explícito do app

Para um futuro comando Done que tenha uma referência a este campo, o caminho compatível pelo responder é solicitar que a própria janela dele chame makeFirstResponder(nil). Verifique o Boolean retornado antes de continuar: a Apple documenta que o responder atual pode se recusar a renunciar ao foco, fazendo o método retornar false. Use a janela real do campo, sem presumir que a janela-chave do app pertence a este editor. Documentação de makeFirstResponder(_:) da Apple

Mantenha o acesso ao AppKit e os retornos de chamada de interface desta integração no ator principal. A declaração atual do delegado está isolada no ator principal, e a documentação do Swift descreve MainActor como usando um executor equivalente à fila principal de despacho. O exemplo expressa esse isolamento diretamente com @MainActor; não adiciona uma tarefa adiada entre a conclusão e a gravação na vinculação. Declaração de retorno de chamada da Apple, documentação de MainActor do Swift

A implementação tem um único limite claro de confirmação: o AppKit termina a edição, o coordenador lê o campo e a vinculação atualiza o texto SwiftUI dependente. Proteger o rascunho durante as atualizações da representação torna esse limite útil para a edição cotidiana de rótulos, sem exigir mecanismos personalizados de layout de texto.

Fontes