Para permitir que uma NSTextView criada programaticamente aceite digitação, habilite a edição, adicione-a à janela e chame makeFirstResponder(_:) dessa janela. A janela também precisa poder se tornar a janela principal. Veja um exemplo completo e uma sequência objetiva de diagnóstico.

O que precisa estar correto para a digitação funcionar?

Este guia aborda uma tarefa: criar uma pequena janela de app para Mac na qual alguém possa digitar e editar texto em várias linhas. Ele usa o AppKit diretamente e, em seguida, explica as escolhas de ciclo de vida correspondentes ao incorporar o editor no SwiftUI.

Três condições distintas importam. A visualização de texto precisa permitir edição, a janela precisa receber eventos do teclado e a visualização de texto deve ser a primeira respondente pretendida nessa janela. A Apple identifica keyWindow do app como a janela que está recebendo eventos do teclado; apenas exibir uma janela não determina qual visualização deve editar o texto. Apple: keyWindow

Definir isEditable = true permite a edição e também torna o texto selecionável. A Apple documenta que as visualizações de texto são editáveis por padrão, mas definir isso explicitamente deixa clara a configuração do exemplo. A editabilidade, por si só, não define o destino do teclado. Apple: isEditable

Para definir o foco, use makeFirstResponder(textView) da janela que contém a visualização. A Apple instrui explicitamente os desenvolvedores a usar esse método em vez de chamar becomeFirstResponder() diretamente na visualização de texto. Isso permite que a janela coordene a transição entre os respondentes. Apple: NSTextView

Crie uma janela editável completa em Swift

O programa independente a seguir tem como alvo o macOS 14 ou posterior e usa uma versão atual do toolchain do Swift com o SDK do macOS. O título fictício da janela é Local Editor, e o texto inicial é Sample note. O texto permanece na memória; este exemplo não implementa salvamento.

Salve o programa completo como EditorDemo.swift. Use-o como executável independente, em vez de adicionar um segundo ponto de entrada a um app existente. O Swift permite apenas um ponto de entrada de programa; o tipo @main abaixo o fornece. The Swift Programming Language: Attributes

import AppKit

@MainActor
final class EditorDelegate: NSObject, NSApplicationDelegate {
    private var editorWindow: NSWindow?

    func applicationDidFinishLaunching(_ notification: Notification) {
        let window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 640, height: 420),
            styleMask: [.titled, .closable, .miniaturizable, .resizable],
            backing: .buffered,
            defer: false
        )
        window.title = "Local Editor"
        window.isReleasedWhenClosed = false

        let scrollView = NSTextView.scrollableTextView()
        guard let textView = scrollView.documentView as? NSTextView else {
            fatalError("Expected an NSTextView document view")
        }

        textView.isEditable = true
        textView.isSelectable = true
        textView.isRichText = false
        textView.allowsUndo = true
        textView.font = NSFont.systemFont(ofSize: 16)
        textView.textContainerInset = NSSize(width: 12, height: 12)
        textView.string = "Sample note\n"
        textView.setSelectedRange(
            NSRange(location: (textView.string as NSString).length, length: 0)
        )

        scrollView.frame = NSRect(x: 0, y: 0, width: 640, height: 420)
        scrollView.autoresizingMask = [.width, .height]
        window.contentView = scrollView
        window.initialFirstResponder = textView
        editorWindow = window

        let mainMenu = NSMenu()
        let appMenuItem = NSMenuItem()
        mainMenu.addItem(appMenuItem)
        let appMenu = NSMenu()
        appMenu.addItem(NSMenuItem(
            title: "Quit Local Editor",
            action: #selector(NSApplication.terminate(_:)),
            keyEquivalent: "q"
        ))
        appMenuItem.submenu = appMenu
        NSApp.mainMenu = mainMenu

        window.center()
        window.makeKeyAndOrderFront(nil)
        NSApp.activate()

        guard textView.window === window,
              textView.acceptsFirstResponder else {
            print("Editor is not ready to receive focus")
            return
        }

        let accepted = window.makeFirstResponder(textView)
        print("Focus request returned:", accepted)
        print("Editor is first responder:", window.firstResponder === textView)
        print("Window is key:", window.isKeyWindow)
    }

    func applicationShouldTerminateAfterLastWindowClosed(
        _ sender: NSApplication
    ) -> Bool {
        true
    }
}

@main
@MainActor
enum EditorDemo {
    static func main() {
        let application = NSApplication.shared
        application.setActivationPolicy(.regular)
        let delegate = EditorDelegate()
        application.delegate = delegate
        withExtendedLifetime(delegate) {
            application.run()
        }
    }
}

Em um Mac com o Xcode ou as Command Line Tools instalados, compile e execute o programa no diretório que contém o arquivo:

xcrun swiftc -parse-as-library \
  -target "$(uname -m)-apple-macos14.0" \
  -framework AppKit EditorDemo.swift -o EditorDemo
./EditorDemo

O exemplo foi analisado com base na documentação consultada, mas não pôde ser compilado nem executado neste ambiente de pesquisa, que usa Linux e não tem um compilador Swift. Verifique o comportamento da janela no macOS com as verificações abaixo.

Por que essa configuração resolve a entrada do teclado

O método de conveniência scrollableTextView() retorna um NSScrollView; o código obtém a visualização de texto por meio de documentView. Manter o editor de texto dentro de uma visualização de rolagem oferece a este pequeno exemplo um contêiner simples para conteúdo em várias linhas. Esse método de criação está disponível a partir do macOS 10.14. Apple: scrollableTextView()

O código adiciona esse contêiner antes de solicitar o foco. Ele também define initialFirstResponder, que a Apple define como a visualização selecionada quando a janela aparece pela primeira vez. A chamada posterior a makeFirstResponder(_:) solicita explicitamente o editor e fornece um resultado que pode ser inspecionado. O foco inicial e uma solicitação de foco posterior correspondem a momentos diferentes do ciclo de vida da janela. Apple: initialFirstResponder, Apple: makeFirstResponder(_:)

A janela usa um estilo com título para poder se tornar a janela principal. makeKeyAndOrderFront(nil) solicita tanto a apresentação da janela quanto seu status de janela principal. A Apple distingue isso de orderFront(_:), que altera a ordem sem mudar a janela principal. Apple: canBecomeKey, Apple: makeKeyAndOrderFront(_:)

A chamada a activate() é o motivo pelo qual este programa específico requer o macOS 14 ou posterior. A Apple descreve a ativação como uma solicitação, não como uma garantia. Se a janela aparecer sem aceitar digitação, ative o app clicando na janela e tente novamente. Não interprete uma impressão imediata de diagnóstico como garantia de que a ativação já foi concluída. Apple: activate()

O ponto de entrada e o delegado são marcados com @MainActor, cujo executor a Apple define como equivalente à fila principal de despacho. O programa também inicia o loop de eventos do AppKit com run(), permitindo que o AppKit despache eventos do usuário após o término da configuração. Apple: MainActor, Apple: run()

Diagnostique um editor visível que ainda não aceita digitação

Siga a sequência abaixo na ordem indicada. Ela separa a conexão à janela, a elegibilidade da janela, a seleção do respondente e a editabilidade, de modo que cada observação indique uma próxima ação específica.

Verificação O que o resultado significa Próxima ação
textView.window === window O editor pertence à janela pretendida. Se for falso, termine de conectar a hierarquia de visualizações antes de solicitar o foco.
window.canBecomeKey A janela pode receber o status de janela principal. Se for falso, inspecione o estilo ou a subclasse personalizada.
window.isKeyWindow Esta é atualmente a janela principal do app. Ative o app e selecione a janela pretendida; depois, verifique novamente.
textView.acceptsFirstResponder O editor aceita uma solicitação de foco. Se for falso, inspecione a configuração e as substituições da subclasse.
window.firstResponder === textView Este editor específico está com o status de primeiro respondente. Se for falso, inspecione a transição de foco e o respondente atual.
textView.isEditable A edição do usuário está habilitada. Defina como true se a edição for desejada.

A verificação do respondente é especialmente útil porque makeFirstResponder(_:) tem uma sutileza documentada: pode retornar true quando o respondente solicitado recusa o status de primeiro respondente e a janela se torna a primeira respondente. A Apple também diz para verificar acceptsFirstResponder antes de fazer a solicitação explicitamente. Portanto, inspecione tanto o valor retornado quanto a identidade resultante de firstResponder. Apple: makeFirstResponder(_:)

Um resultado false pode significar que o respondente atual se recusou a perder o status de primeiro respondente. Analise o comportamento desse respondente e qualquer delegado envolvido na edição. Por exemplo, a Apple documenta que o delegado de uma visualização de texto pode rejeitar o encerramento da edição por meio de textShouldEndEditing(_:). Repetir a mesma solicitação de foco sem resolver a recusa não trata sua causa. Apple: makeFirstResponder(_:), Apple: NSTextView

Para uma verificação manual básica, digite Blue square, pressione Return, digite Green circle e, em seguida, use as teclas de seta e Delete para editar o texto. Redimensione a janela e continue digitando. Essas são etapas de verificação propostas, não resultados de testes relatados. Feche a janela quando terminar; a amostra será encerrada.

O que muda em uma janela sem bordas?

Uma janela sem bordas visível ainda pode não conseguir se tornar a janela principal. A Apple documenta que uma NSWindow comum sem o estilo com título não se torna a janela principal por padrão. A personalização compatível consiste em substituir canBecomeKey em uma subclasse de NSWindow e retornar true quando essa janela deve receber entrada do teclado. Apple: canBecomeKey

Comece pelo exemplo com título para estabelecer a entrada de texto funcional. Se a interface posteriormente exigir uma janela sem bordas, altere a implementação da janela e mantenha as mesmas verificações de conexão, editabilidade e respondente. Alterar a aparência da janela não elimina esses requisitos.

Como o mesmo editor deve funcionar dentro do SwiftUI?

Use NSViewRepresentable para integrar uma visualização de texto do AppKit a uma hierarquia do SwiftUI. A Apple atribui a criação e a configuração inicial a makeNSView(context:), as atualizações de estado contínuas a updateNSView(_:context:) e a coordenação de mensagens de delegado ou target-action a um coordenador. Apple: NSViewRepresentable

Para este editor, uma solução prática é retornar a visualização de rolagem como a visualização representada e manter uma referência à visualização de texto do documento no coordenador ou em um contêiner personalizado. Configure a editabilidade durante a criação. Encaminhe as alterações de texto feitas pelo usuário por meio do delegado quando a interface SwiftUI ao redor precisar de um binding.

Trate a conexão à janela como um evento separado da criação. Antes de solicitar o foco, verifique se window do editor não é nil. Uma subclasse de AppKit pode usar viewDidMoveToWindow() para observar alterações na hierarquia; a Apple documenta que, nesse contexto, um window nil significa que a visualização não está atualmente em nenhuma janela. Isso fornece um ponto de ciclo de vida documentado para implementar uma solicitação de foco que leve em conta a conexão à janela. Apple: viewDidMoveToWindow()

Como política de implementação, solicite o foco inicial uma vez para a apresentação pretendida ou em resposta a uma ação explícita, como Focus Editor. Evite chamar makeFirstResponder(_:) incondicionalmente a cada atualização do SwiftUI: essa política moveria o foco repetidamente de volta depois que a pessoa selecionasse outro controle. Verifique a janela que contém o editor no momento da solicitação, em vez de presumir que uma janela armazenada anteriormente continua sendo adequada.

Evite também redefinir a string do editor em cada atualização. Em um wrapper baseado em binding, compare o estado recebido com o texto atual e leve em conta o texto marcado e a seleção antes de substituir o conteúdo. O exemplo independente inicializa sua string uma vez; um editor sincronizado precisa de uma política de atualização explícita.

Deixe o SwiftUI dimensionar a visualização raiz representada. A Apple alerta que alterar diretamente frame ou bounds dessa visualização entra em conflito com o gerenciamento de layout do SwiftUI. Portanto, a configuração direta do frame no programa independente do AppKit não é um padrão de dimensionamento a ser copiado para uma visualização raiz gerenciada pelo SwiftUI. Apple: NSViewRepresentable

Aceitar digitação exige uma pilha TextKit personalizada?

Para esta tarefa, use o comportamento de edição existente da NSTextView. A Apple a descreve como a interface do sistema de texto que desenha o texto e lida com seleção, modificação, gerenciamento de entrada e associações de teclas. A digitação comum não exige reconstruir esses recursos. Apple: NSTextView

A seleção do mecanismo TextKit é uma decisão separada do foco do teclado. A Apple documenta que init(frame:) usa NSLayoutManager por padrão, enquanto uma visualização de texto montada manualmente pode usar NSTextLayoutManager no macOS 12 ou posterior por meio do inicializador designado. Escolha um mecanismo com base nos requisitos de layout; mudar de mecanismo não substitui tornar o editor o primeiro respondente. Apple: init(frame:textContainer:)

Um diagnóstico merece atenção: ler layoutManager não é necessariamente uma inspeção passiva. A Apple documenta que acessá-lo em uma visualização de texto ou contêiner de texto que usa o sistema mais novo pode acionar o modo de compatibilidade com NSLayoutManager. O exemplo evita consultá-lo e não presume qual mecanismo a fábrica de conveniência de rolagem escolhe. Apple: NSTextView

Um resultado funcional é concreto: a janela pretendida do app está ativa e é a janela principal, sua visualização de texto editável é a primeira respondente e a digitação altera o texto exibido. Estabeleça esse comportamento primeiro com a pequena janela e, depois, mantenha as mesmas verificações ao adicionar integração com SwiftUI, layout personalizado ou controles adicionais.

Fontes