Per fare in modo che un NSTextView creato programmaticamente accetti la digitazione, abilita la modifica, aggiungilo alla finestra e chiama makeFirstResponder(_:) su quella finestra. Anche la finestra deve poter diventare key. Ecco un esempio completo e una sequenza mirata per la risoluzione dei problemi.

Cosa deve essere vero prima che la digitazione funzioni?

Questa guida illustra un’unica attività: creare una piccola finestra per un’app Mac in cui sia possibile digitare e modificare testo su più righe. Usa direttamente AppKit, poi spiega le corrispondenti scelte relative al ciclo di vita quando si integra l’editor in SwiftUI.

Contano tre condizioni distinte. La visualizzazione del testo deve consentire la modifica, la finestra deve ricevere gli eventi della tastiera e la visualizzazione del testo deve essere il primo responder previsto all’interno di quella finestra. Apple identifica keyWindow dell’applicazione come la finestra che sta ricevendo gli eventi della tastiera; visualizzare una finestra, da solo, non determina quale vista debba modificare il testo. Apple: keyWindow

Impostare isEditable = true consente la modifica e rende selezionabile anche la visualizzazione del testo. Apple documenta che le visualizzazioni di testo sono modificabili per impostazione predefinita, ma impostare esplicitamente questa proprietà rende chiara la configurazione dell’esempio. La sola modificabilità non determina la destinazione della tastiera. Apple: isEditable

Per impostare il focus, usa makeFirstResponder(textView) della finestra contenitore. Apple indica esplicitamente agli sviluppatori di usare questo metodo invece di chiamare direttamente becomeFirstResponder() sulla visualizzazione del testo. In questo modo la finestra può coordinare il passaggio tra responder. Apple: NSTextView

Crea una finestra modificabile completa in Swift

Il programma autonomo seguente è destinato a macOS 14 o versioni successive e usa una toolchain Swift attuale con l’SDK macOS. Il titolo fittizio della finestra è Local Editor e il testo iniziale è Sample note. Il testo rimane in memoria; questo esempio non implementa il salvataggio.

Salva il programma completo come EditorDemo.swift. Usalo come eseguibile autonomo, invece di aggiungere un secondo punto di ingresso a un’app esistente. Swift consente un solo punto di ingresso per programma; il tipo @main seguente lo fornisce. 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()
        }
    }
}

Su un Mac con Xcode o Command Line Tools installati, compila e avvia il programma dalla directory che contiene il file:

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

L’esempio è stato confrontato con la documentazione recuperata, ma non è stato possibile compilarlo o provarlo nell’ambiente di ricerca, che usa Linux e non dispone di un compilatore Swift. Verifica il comportamento della finestra su macOS con i controlli riportati di seguito.

Perché questa configurazione gestisce l’input da tastiera

Il metodo di convenienza scrollableTextView() restituisce un NSScrollView; il codice recupera la visualizzazione del testo tramite documentView. Racchiudere l’editor di testo in una visualizzazione a scorrimento offre a questo semplice esempio un contenitore pratico per i contenuti su più righe. Questo metodo factory è disponibile da macOS 10.14. Apple: scrollableTextView()

Il codice aggiunge il contenitore prima di richiedere il focus. Imposta anche initialFirstResponder, che Apple definisce come la vista selezionata quando la finestra viene mostrata per la prima volta. La successiva chiamata a makeFirstResponder(_:) richiede esplicitamente l’editor e restituisce un risultato da controllare. Il focus iniziale e una richiesta di focus successiva intervengono in momenti diversi del ciclo di vita della finestra. Apple: initialFirstResponder, Apple: makeFirstResponder(_:)

La finestra usa uno stile con titolo, così può diventare key. makeKeyAndOrderFront(nil) richiede sia la visualizzazione della finestra sia il suo stato key. Apple distingue questo metodo da orderFront(_:), che modifica l’ordine senza cambiare la finestra key. Apple: canBecomeKey, Apple: makeKeyAndOrderFront(_:)

La chiamata a activate() è il motivo per cui questo programma richiede macOS 14 o versioni successive. Apple descrive l’attivazione come una richiesta, non come una garanzia. Se la finestra appare ma non riceve la digitazione, attiva l’app facendo clic sulla sua finestra e riprova. Non interpretare una stampa diagnostica immediata come garanzia che l’attivazione sia già stata completata. Apple: activate()

Il punto di ingresso e il delegato sono contrassegnati con @MainActor, il cui executor è definito da Apple come equivalente alla coda di dispatch principale. Il programma avvia anche il ciclo degli eventi di AppKit con run(), così che AppKit possa distribuire gli eventi dell’utente al termine della configurazione. Apple: MainActor, Apple: run()

Individua il problema quando l’editor è visibile ma non accetta la digitazione

Segui questa sequenza nell’ordine indicato. Distingue tra aggiunta alla gerarchia, idoneità della finestra, selezione del responder e possibilità di modifica, così ogni osservazione indica un’azione successiva specifica.

Controllo Significato del risultato Azione successiva
textView.window === window L’editor appartiene alla finestra prevista. Se è falso, completa l’aggiunta della gerarchia di viste prima di richiedere il focus.
window.canBecomeKey La finestra può ricevere lo stato key. Se è falso, controlla lo stile o una sottoclasse personalizzata.
window.isKeyWindow Questa finestra è attualmente key nell’app. Attiva l’app e seleziona la finestra prevista, poi ricontrolla.
textView.acceptsFirstResponder L’editor accetta una richiesta di focus. Se è falso, controlla la configurazione e gli override delle sottoclassi.
window.firstResponder === textView Questo specifico editor è il primo responder. Se è falso, controlla il passaggio del focus e il responder attuale.
textView.isEditable Le modifiche dell’utente sono abilitate. Impostalo su true se si desidera consentire la modifica.

Il controllo del responder è particolarmente utile perché makeFirstResponder(_:) presenta una sottigliezza documentata: può restituire true quando il responder richiesto rifiuta di diventare il primo responder e la finestra assume invece quel ruolo. Apple indica anche di controllare acceptsFirstResponder prima di effettuare esplicitamente la richiesta. Esamina quindi sia il valore restituito sia l’identità di firstResponder risultante. Apple: makeFirstResponder(_:)

Un risultato false può indicare che il primo responder esistente si è rifiutato di rinunciare al proprio ruolo. Controlla il comportamento di quel responder e l’eventuale delegato coinvolto nella modifica. Per esempio, Apple documenta che il delegato di una visualizzazione del testo può rifiutare la fine della modifica tramite textShouldEndEditing(_:). Ripetere la stessa richiesta di focus senza risolvere il rifiuto non ne affronta la causa. Apple: makeFirstResponder(_:), Apple: NSTextView

Per un controllo manuale di base, digita Blue square, premi Invio, digita Green circle, poi usa i tasti freccia e Canc per modificare il testo. Ridimensiona la finestra e continua a digitare. Sono passaggi di verifica proposti, non risultati di test dichiarati. Quando hai finito, chiudi la finestra; l’esempio termina.

Cosa cambia con una finestra senza bordi?

Una finestra senza bordi visibile potrebbe non essere in grado di diventare key. Apple documenta che, per impostazione predefinita, un normale NSWindow senza lo stile con titolo non diventa key. La personalizzazione supportata consiste nell’override di canBecomeKey in una sottoclasse di NSWindow e nel restituire true quando la finestra deve ricevere l’input da tastiera. Apple: canBecomeKey

Inizia con l’esempio con titolo per verificare il funzionamento dell’inserimento del testo. Se in seguito l’interfaccia richiede una finestra senza bordi, modifica l’implementazione della finestra mantenendo gli stessi controlli per l’aggiunta alla gerarchia, la possibilità di modifica e il responder. Cambiare l’aspetto della finestra non elimina questi requisiti.

Come deve funzionare lo stesso editor in SwiftUI?

Usa NSViewRepresentable per integrare una visualizzazione di testo AppKit in una gerarchia SwiftUI. Apple assegna la creazione e la configurazione iniziale a makeNSView(context:), gli aggiornamenti continui dello stato a updateNSView(_:context:) e il coordinamento dei messaggi di delegato o target-action a un coordinator. Apple: NSViewRepresentable

Per questo editor, una soluzione pratica consiste nel restituire la visualizzazione a scorrimento come vista rappresentata e conservare un riferimento alla visualizzazione del testo del documento nel coordinator o in un contenitore personalizzato. Configura la possibilità di modifica durante la creazione. Inoltra le modifiche del testo tramite il delegato se l’interfaccia SwiftUI circostante deve aggiornare un binding.

Considera l’aggiunta alla gerarchia come un evento distinto dalla creazione. Prima di richiedere il focus, controlla che window dell’editor non sia nil. Una sottoclasse AppKit può usare viewDidMoveToWindow() per rilevare i cambiamenti della gerarchia; Apple documenta che un window nil in quel punto significa che la vista al momento non si trova in alcuna finestra. Questo fornisce un hook documentato nel ciclo di vita per implementare una richiesta di focus che tenga conto dell’aggiunta alla gerarchia. Apple: viewDidMoveToWindow()

Come criterio di implementazione, richiedi il focus iniziale una sola volta per la presentazione prevista, oppure in risposta a un’azione esplicita come Focus Editor. Evita di chiamare incondizionatamente makeFirstResponder(_:) a ogni aggiornamento SwiftUI: con questo criterio il focus tornerebbe continuamente all’editor dopo che la persona ha selezionato un altro controllo. Al momento della richiesta, controlla la finestra contenitore invece di presumere che una finestra memorizzata in precedenza sia ancora quella appropriata.

Evita anche di reimpostare la stringa dell’editor a ogni aggiornamento. In un wrapper basato su binding, confronta lo stato in ingresso con il testo attuale e tieni conto del testo contrassegnato e della selezione prima di sostituire il contenuto. L’esempio autonomo inizializza la stringa una sola volta; un editor sincronizzato richiede una strategia di aggiornamento esplicita.

Lascia che SwiftUI determini le dimensioni della vista radice rappresentata. Apple avverte che modificare direttamente frame o bounds di quella vista entra in conflitto con la gestione del layout di SwiftUI. La configurazione diretta della cornice nel programma AppKit autonomo non è quindi un modello di dimensionamento da applicare a una vista radice gestita da SwiftUI. Apple: NSViewRepresentable

Per accettare la digitazione serve una pila TextKit personalizzata?

Per questa attività, usa il comportamento di modifica già presente in NSTextView. Apple lo descrive come il componente front-end del sistema di testo che disegna il testo e gestisce selezione, modifica, gestione dell’input e associazioni di tasti. La normale digitazione non richiede di ricreare queste funzionalità. Apple: NSTextView

La scelta del motore TextKit è distinta da quella del focus della tastiera. Apple documenta che init(frame:) usa NSLayoutManager per impostazione predefinita, mentre una visualizzazione del testo assemblata manualmente può usare NSTextLayoutManager da macOS 12 in poi tramite l’inizializzatore designato. Scegli il motore in base ai requisiti di layout; cambiare motore non sostituisce l’impostazione dell’editor come primo responder. Apple: init(frame:textContainer:)

Un dettaglio diagnostico richiede attenzione: leggere layoutManager non è necessariamente un’ispezione passiva. Apple documenta che accedervi da una visualizzazione del testo o da un contenitore di testo che usa il sistema più recente può attivare una modalità di compatibilità basata su NSLayoutManager. L’esempio evita di leggerlo e non presume quale motore scelga il metodo factory di convenienza per la visualizzazione a scorrimento. Apple: NSTextView

Il risultato atteso è concreto: la finestra prevista dell’app è attiva e key, la sua visualizzazione del testo modificabile è il primo responder e la digitazione modifica il testo visualizzato. Verifica questo comportamento prima con la piccola finestra, poi mantieni gli stessi controlli quando aggiungi l’integrazione SwiftUI, un layout personalizzato o altri controlli.

Fonti