Para que un NSTextView creado mediante programación acepte escritura, activa la edición, añádelo a tu ventana y llama a makeFirstResponder(_:) de esa ventana. La ventana también debe poder convertirse en la ventana clave. Aquí tienes un ejemplo completo y una secuencia de diagnóstico específica.

¿Qué debe cumplirse para que funcione la escritura?

Esta guía aborda una tarea: crear una pequeña ventana de una app para Mac donde se pueda escribir y editar texto en varias líneas. Usa AppKit directamente y luego explica las decisiones de ciclo de vida correspondientes al integrar el editor en SwiftUI.

Hay tres condiciones independientes. La vista de texto debe permitir la edición, la ventana debe recibir eventos de teclado y la vista de texto debe ser el primer respondedor previsto dentro de esa ventana. Apple identifica keyWindow de la aplicación como la ventana que recibe actualmente los eventos de teclado; mostrar una ventana, por sí solo, no determina qué vista debe editar el texto. Apple: keyWindow

Al establecer isEditable = true, se permite la edición y también se puede seleccionar el texto. Apple documenta que las vistas de texto son editables de forma predeterminada, pero establecerlo explícitamente aclara la configuración del ejemplo. La posibilidad de editar, por sí sola, no determina dónde se escribirá con el teclado. Apple: isEditable

Para establecer el foco, usa makeFirstResponder(textView) de la ventana contenedora. Apple indica explícitamente a los desarrolladores que usen este método en lugar de llamar directamente a becomeFirstResponder() de la vista de texto. Así, la ventana puede coordinar la transición entre los respondedores. Apple: NSTextView

Crea una ventana editable completa en Swift

El siguiente programa independiente está dirigido a macOS 14 o posterior y usa una versión actual de Swift con el SDK de macOS. El título ficticio de la ventana es Local Editor y el texto inicial es Sample note. El texto permanece en memoria; este ejemplo no implementa el guardado.

Guarda el programa completo como EditorDemo.swift. Úsalo como ejecutable independiente en lugar de añadir un segundo punto de entrada a una app existente. Swift permite un solo punto de entrada al programa; el tipo @main de abajo lo proporciona. El lenguaje de programación Swift: atributos

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

En un Mac con Xcode o las herramientas de línea de comandos instaladas, compílalo y ejecútalo desde el directorio que contiene el archivo:

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

El ejemplo se revisó comparándolo con la documentación consultada, pero no se pudo compilar ni ejecutar en este entorno de investigación, que usa Linux y no tiene un compilador de Swift. Comprueba el comportamiento de la ventana en macOS con las verificaciones siguientes.

Por qué esta configuración permite la entrada de teclado

El método de conveniencia scrollableTextView() devuelve un NSScrollView; el código obtiene su vista de texto mediante documentView. Al mantener el editor de texto dentro de una vista de desplazamiento, este pequeño ejemplo cuenta con un contenedor sencillo para el contenido en varias líneas. La fábrica está disponible desde macOS 10.14. Apple: scrollableTextView()

El código añade ese contenedor antes de solicitar el foco. También establece initialFirstResponder, que Apple define como la vista seleccionada cuando aparece la ventana por primera vez. La llamada posterior a makeFirstResponder(_:) solicita explícitamente el editor y permite inspeccionar el resultado. El foco inicial y una solicitud posterior de foco corresponden a momentos distintos del ciclo de vida de la ventana. Apple: initialFirstResponder, Apple: makeFirstResponder(_:)

La ventana usa un estilo con título para poder convertirse en la ventana clave. makeKeyAndOrderFront(nil) solicita tanto mostrar la ventana como activarla. Apple distingue este método de orderFront(_:), que cambia el orden sin cambiar la ventana clave. Apple: canBecomeKey, Apple: makeKeyAndOrderFront(_:)

La llamada a activate() es la razón por la que este programa en particular requiere macOS 14 o posterior. Apple describe la activación como una solicitud, no como una garantía. Si aparece la ventana, pero no recibe escritura, activa la app haciendo clic en ella y vuelve a comprobarlo. No interpretes un mensaje de diagnóstico inmediato como garantía de que la activación ya se completó. Apple: activate()

El punto de entrada y el delegado llevan la anotación @MainActor, cuyo ejecutor Apple define como equivalente a la cola principal de GCD. El programa también inicia el bucle de eventos de AppKit con run(), lo que permite a AppKit distribuir los eventos del usuario una vez finalizada la configuración. Apple: MainActor, Apple: run()

Diagnostica un editor visible que aun así no acepta escritura

Sigue esta secuencia en orden. Distingue la incorporación a la ventana, la aptitud de esta para recibir el estado de ventana clave, la selección del respondedor y la posibilidad de editar, de modo que cada observación indique una acción concreta.

Comprobación Qué significa el resultado Siguiente acción
textView.window === window El editor pertenece a la ventana prevista. Si es falso, termina de incorporar la jerarquía de vistas antes de solicitar el foco.
window.canBecomeKey La ventana puede recibir el estado de ventana clave. Si es falso, revisa su estilo o su subclase personalizada.
window.isKeyWindow Esta ventana es actualmente la ventana clave de la app. Activa la app y selecciona la ventana prevista; luego vuelve a comprobarlo.
textView.acceptsFirstResponder El editor acepta una solicitud de foco. Si es falso, revisa la configuración y las redefiniciones de la subclase.
window.firstResponder === textView Este editor concreto tiene el estado de primer respondedor. Si es falso, revisa la transición del foco y el respondedor actual.
textView.isEditable La edición del usuario está habilitada. Si se pretende permitir la edición, establécelo en true.

La comprobación del respondedor es especialmente útil porque makeFirstResponder(_:) tiene un matiz documentado: puede devolver true si el respondedor solicitado rechaza el estado de primer respondedor y la ventana se convierte en primer respondedor. Apple también indica que se compruebe acceptsFirstResponder antes de realizar la solicitud explícitamente. Por tanto, inspecciona tanto el valor devuelto como la identidad de firstResponder resultante. Apple: makeFirstResponder(_:)

Un resultado false puede significar que el primer respondedor actual se negó a ceder el puesto. Revisa el comportamiento de ese respondedor y cualquier delegado de edición involucrado. Por ejemplo, Apple documenta que el delegado de una vista de texto puede rechazar el fin de la edición mediante textShouldEndEditing(_:). Repetir la misma solicitud de foco sin resolver el rechazo no aborda su causa. Apple: makeFirstResponder(_:), Apple: NSTextView

Para una comprobación manual básica, escribe Blue square, pulsa Retorno, escribe Green circle y luego usa las teclas de flecha y Suprimir para editar el texto. Cambia el tamaño de la ventana y sigue escribiendo. Estos son pasos de verificación propuestos, no resultados de pruebas. Cierra la ventana al terminar; entonces se cerrará el ejemplo.

¿Qué cambia con una ventana sin bordes?

Una ventana visible sin bordes quizá no pueda convertirse en la ventana clave. Apple documenta que, de forma predeterminada, un NSWindow normal sin el estilo con título no se convierte en ventana clave. La personalización admitida consiste en redefinir canBecomeKey en una subclase de NSWindow y devolver true cuando se pretenda que esa ventana reciba entradas de teclado. Apple: canBecomeKey

Empieza con el ejemplo con título para comprobar que la escritura funciona. Si más adelante la interfaz requiere una ventana sin bordes, cambia la implementación de la ventana y conserva las mismas comprobaciones de incorporación, edición y respondedor. Cambiar la apariencia de la ventana no elimina esos requisitos.

¿Cómo debería funcionar el mismo editor dentro de SwiftUI?

Usa NSViewRepresentable para integrar una vista de texto de AppKit en una jerarquía de SwiftUI. Apple asigna la creación y configuración inicial a makeNSView(context:), las actualizaciones continuas del estado a updateNSView(_:context:) y la coordinación con los mensajes del delegado o de destino y acción a un coordinador. Apple: NSViewRepresentable

Para este editor, un diseño práctico consiste en devolver la vista de desplazamiento como vista representada y guardar una referencia a la vista de texto del documento en el coordinador o en un contenedor personalizado. Configura la posibilidad de editar durante la creación. Si la interfaz de SwiftUI que lo rodea necesita un enlace, propaga mediante el delegado los cambios de texto que haga el usuario.

Trata la incorporación a la ventana como un evento separado de la creación. Antes de solicitar el foco, comprueba que window del editor no sea nil. Una subclase de AppKit puede usar viewDidMoveToWindow() para observar los cambios en la jerarquía; Apple documenta que un window nil en ese punto significa que la vista no reside actualmente en ninguna ventana. Esto proporciona un punto de enlace documentado para implementar una solicitud de foco que tenga en cuenta la incorporación. Apple: viewDidMoveToWindow()

Como pauta de implementación, solicita el foco inicial una vez para la presentación prevista o como respuesta a una acción explícita, como Focus Editor. Evita llamar incondicionalmente a makeFirstResponder(_:) en cada actualización de SwiftUI: esa pauta devolvería repetidamente el foco al editor después de que la persona seleccionara otro control. Comprueba la ventana contenedora en el momento de la solicitud en lugar de dar por hecho que una ventana almacenada anteriormente sigue siendo la adecuada.

Evita también restablecer la cadena de texto del editor en cada actualización. En un contenedor basado en enlaces, compara el estado entrante con el texto actual y ten en cuenta el texto marcado y la selección antes de reemplazar el contenido. El ejemplo independiente inicializa su cadena una sola vez; un editor sincronizado necesita una política de actualización explícita.

Deja que SwiftUI dimensione la vista raíz representada. Apple advierte que cambiar directamente su frame o bounds entra en conflicto con la gestión del diseño de SwiftUI. Por tanto, la configuración directa del marco en el programa independiente de AppKit no es un patrón de dimensionamiento que debas copiar para una vista raíz gestionada por SwiftUI. Apple: NSViewRepresentable

¿Hace falta una pila TextKit personalizada para aceptar escritura?

Para esta tarea, usa el comportamiento de edición existente de NSTextView. Apple lo describe como la interfaz del sistema de texto que dibuja el texto y gestiona la selección, la modificación, la entrada y los atajos de teclado. Para escribir con normalidad no hace falta reconstruir estas funciones. Apple: NSTextView

La elección del motor TextKit es una decisión independiente del foco del teclado. Apple documenta que init(frame:) usa NSLayoutManager de forma predeterminada, mientras que una vista de texto ensamblada manualmente puede usar NSTextLayoutManager en macOS 12 o posterior mediante el inicializador designado. Elige un motor según tus requisitos de diseño; cambiar de motor no sustituye la necesidad de convertir el editor en primer respondedor. Apple: init(frame:textContainer:)

Hay que tener cuidado con un diagnóstico: leer layoutManager no es necesariamente una inspección pasiva. Apple documenta que acceder a él en una vista de texto o un contenedor de texto que use el sistema más reciente puede activar el modo de compatibilidad con NSLayoutManager. El ejemplo evita consultarlo y no presupone qué motor elige la fábrica de vistas de desplazamiento. Apple: NSTextView

El resultado esperado es concreto: la ventana prevista de la app está activa y es la ventana clave, su vista de texto editable es el primer respondedor y la escritura modifica el texto mostrado. Comprueba primero ese comportamiento con la ventana pequeña y luego conserva las mismas verificaciones al añadir la integración con SwiftUI, un diseño personalizado u otros controles.

Fuentes