Per rendere visibile il punto di inserimento nell’NSTextView integrato della tua app per Mac, ripristina il focus della tastiera e chiama scrollRangeToVisible(_:) con la selezione corrente. Configura separatamente il colore del cursore. Mantenere il cursore visibile nell’area dello schermo e impedirgli di lampeggiare richiede controlli diversi.
Questa guida crea un piccolo editor locale con un pulsante Rivela punto di inserimento. Affronta un’attività comune nelle app desktop: tornare al punto di modifica dopo aver scorso altrove nel documento. L’implementazione conserva la selezione corrente e lascia che AppKit gestisca il normale inserimento del testo e il disegno del cursore.
Individua il motivo per cui il punto di inserimento non è visibile
Parti dal sintomo visibile. Scorrimento, focus, selezione e animazione del cursore possono produrre risultati simili, ma ognuno richiede una risposta diversa.
| Cosa osservi | Cosa controllare | Azione appropriata |
|---|---|---|
| Il punto in cui stai modificando il documento è fuori dall’area visibile | La selezione corrente della vista del testo | Fai scorrere l’intervallo finché non è visibile |
| Facendo clic su un altro controllo non puoi più scrivere nell’editor | Il primo responder della finestra | Ripristina esplicitamente il focus quando richiesto |
| È difficile distinguere il cursore dallo sfondo | I colori del cursore e dello sfondo | Configura le proprietà del colore della vista del testo |
| Il testo è evidenziato invece di mostrare un singolo cursore | Se l’intervallo selezionato ha una lunghezza diversa da zero | Conserva la selezione, a meno che l’utente non richieda di spostarla |
| Il cursore scompare e riappare mentre l’editor è attivo | La normale animazione del cursore | Considera il lampeggiamento separatamente dallo scorrimento |
Apple documenta scrollRangeToVisible(_:) come metodo che fa scorrere la vista di scorrimento contenitrice finché i primi caratteri dell’intervallo fornito non sono visibili. Non garantisce di centrare l’intervallo né di visualizzare per intero una selezione lunga. È quindi adatto a rendere visibile un punto di inserimento senza dover calcolare personalmente i rettangoli dei caratteri. Documentazione Apple sullo scorrimento.
Il focus è gestito dalla finestra contenitrice. Apple specifica che chi chiama deve usare window.makeFirstResponder(textView) anziché invocare direttamente becomeFirstResponder() sulla vista del testo. L’implementazione seguente segue questa procedura e verifica quale oggetto sia effettivamente diventato il primo responder. Documentazione Apple su NSTextView, documentazione sulla transizione del primo responder.
Crea un editor locale con un comando per rivelare il punto di inserimento
In Xcode, crea un progetto per macOS usando SwiftUI e Swift. Per questo esempio, imposta come destinazione di distribuzione macOS 13 o versioni successive. Sostituisci il punto di ingresso dell’app generato con il primo file riportato di seguito e aggiungi il secondo file allo stesso target dell’app. Insieme, questi file costituiscono l’app dimostrativa completa.
L’esempio usa testo sintetico e conserva le modifiche nella vista AppKit per tutta la durata di quest’ultima. Non implementa il salvataggio dei documenti né usa un modello di testo esterno. Chiudendo l’app, le modifiche vengono perse. Il codice è stato verificato rispetto alla documentazione consultata, ma qui non è stato compilato né eseguito perché l’ambiente disponibile è Linux e non include l’SDK di AppKit.
Crea la finestra SwiftUI
Il pulsante incrementa un numero di richiesta. In questo modo, il wrapper riceve un comando specifico da elaborare, anziché riportare il focus sull’editor o farlo scorrere a ogni aggiornamento di SwiftUI.
// CaretDemoApp.swift
import SwiftUI
@main
struct CaretDemoApp: App {
var body: some Scene {
WindowGroup("Local Editor") {
EditorScreen()
}
}
}
struct EditorScreen: View {
@State private var revealRequest = 0
private let sampleText = (1...80)
.map { "Sample row \($0): blue square, green circle." }
.joined(separator: "\n")
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Button("Reveal insertion point") {
revealRequest += 1
}
CaretEditor(
initialText: sampleText,
revealRequest: revealRequest
)
}
.padding()
.frame(minWidth: 480, minHeight: 320)
}
}
Integra l’editor AppKit scorrevole
NSViewRepresentable è il bridge di Apple per gestire una vista AppKit in una gerarchia SwiftUI. In questo caso, la vista rappresentata è un NSScrollView con un NSTextView come vista del documento. Il coordinatore memorizza l’ultima richiesta gestita. Apple fornisce la factory per la vista del testo scorrevole usata durante la creazione. NSViewRepresentable, scrollableTextView().
// CaretEditor.swift
import AppKit
import SwiftUI
@MainActor
struct CaretEditor: NSViewRepresentable {
let initialText: String
let revealRequest: Int
@MainActor
final class Coordinator {
var lastHandledRequest: Int
init(lastHandledRequest: Int) {
self.lastHandledRequest = lastHandledRequest
}
}
func makeCoordinator() -> Coordinator {
Coordinator(lastHandledRequest: revealRequest)
}
func makeNSView(context: Context) -> NSScrollView {
let scrollView = NSTextView.scrollableTextView()
scrollView.hasVerticalScroller = true
guard let textView = scrollView.documentView as? NSTextView else {
preconditionFailure("Expected an NSTextView document view")
}
textView.isRichText = false
textView.isEditable = true
textView.isSelectable = true
textView.allowsUndo = true
textView.drawsBackground = true
textView.backgroundColor = .textBackgroundColor
textView.textColor = .textColor
textView.insertionPointColor = .textColor
textView.font = .systemFont(ofSize: 15)
textView.textContainerInset = NSSize(width: 12, height: 12)
textView.string = initialText
let end = (initialText as NSString).length
textView.setSelectedRange(NSRange(location: end, length: 0))
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard revealRequest != context.coordinator.lastHandledRequest else {
return
}
context.coordinator.lastHandledRequest = revealRequest
guard let textView = scrollView.documentView as? NSTextView,
let window = textView.window,
textView.acceptsFirstResponder else {
return
}
guard window.makeFirstResponder(textView),
window.firstResponder === textView else {
return
}
textView.scrollRangeToVisible(textView.selectedRange())
}
}
L’editor inizia con una selezione di lunghezza zero alla fine del testo di esempio. Premendo Rivela punto di inserimento, l’editor riceve il focus e quella posizione diventa visibile. Le pressioni successive usano la selezione corrente, quindi spostando altrove il cursore cambia la posizione che il comando rende visibile. Il comando non sostituisce il testo né sposta la selezione.
La posizione finale usa NSString.length, che conta le unità di codice UTF-16. La vista UTF-16 di Swift è compatibile con le API NSString indicizzate, mentre String.count di Swift conta i caratteri. Quando crei intervalli di testo AppKit, mantieni coerenti queste rappresentazioni. Leggere un selectedRange() esistente evita di calcolare un offset solo per il comando di visualizzazione. NSString length, Swift String.UTF16View, Swift String.count.
Conserva lo stato di modifica durante gli aggiornamenti di SwiftUI
La scelta progettuale fondamentale consiste nel distinguere la creazione dai comandi. makeNSView inizializza l’editor una sola volta. updateNSView risponde a un cambiamento del numero di richiesta senza modificare il contenuto del documento. Questo rispecchia il ciclo di vita dei representable descritto da Apple per la creazione e l’aggiornamento delle viste AppKit. SwiftUI controlla anche il layout della vista rappresentata, quindi questo wrapper non assegna il frame né i bounds della vista di scorrimento. Indicazioni Apple sul ciclo di vita e sul layout dei representable.
In questo esempio, evita di assegnare textView.string = initialText a ogni aggiornamento. Così facendo, il documento corrente dell’utente verrebbe sostituito con il testo di esempio originale. Analogamente, impostare incondizionatamente la selezione alla fine del documento farebbe sì che un’operazione di visualizzazione spostasse il punto di modifica.
Per un editor basato sullo stato di SwiftUI, aggiungi una sincronizzazione bidirezionale esplicita: inoltra le modifiche dell’utente tramite un coordinatore e definisci quando le modifiche in arrivo dal modello sostituiscono il testo. Conserva le selezioni valide durante le sostituzioni e definisci come risolvere le modifiche simultanee. Questa è una responsabilità distinta dal rendere visibile il cursore; l’esempio mantiene intenzionalmente un solo proprietario dei contenuti modificabili. Apple documenta i coordinatori come il meccanismo per inoltrare a SwiftUI le interazioni di delegati e target-action. Indicazioni sui coordinatori.
Anche i controlli sul focus sono importanti. makeFirstResponder(_:) può fallire se il responder attuale rifiuta di rinunciare al ruolo. Apple osserva inoltre che, se il responder richiesto non accetta il focus, il metodo può restituire un esito positivo lasciando la finestra stessa come primo responder. Verificare window.firstResponder === textView permette di controllare il risultato effettivo prima di far scorrere il documento. Valori restituiti e comportamento del primo responder.
Se l’editor non è associato a una finestra, questo esempio consuma la richiesta senza intervenire. Premi di nuovo il pulsante dopo che l’editor è stato associato. Per una funzione che assegna il focus iniziale, progetta una richiesta esplicita che tenga conto dell’associazione alla finestra, anziché riprendere ripetutamente il focus durante aggiornamenti non correlati.
Configura il colore senza sostituire il disegno del cursore
L’esempio assegna .textColor sia al testo sia al punto di inserimento, e .textBackgroundColor allo sfondo dell’editor. Sono colori semantici documentati per il testo e per l’area retrostante. insertionPointColor è la proprietà documentata per impostare il colore del cursore. Verifica l’aspetto risultante su ogni versione di macOS e in ogni modalità di visualizzazione supportata dalla tua app. Colore del cursore, colore del testo, colore dello sfondo del testo.
La configurazione del colore non assegna il focus, non fa scorrere il documento e non imposta una modalità di visualizzazione senza lampeggiamento. Quando individui la causa di un cursore non visibile, mantieni separate queste responsabilità. Per esempio, cambiare il colore non può rendere visibile una posizione che si trova ancora fuori dall’area visibile della vista di scorrimento.
L’esempio evita anche di accedere a layoutManager per calcolare la geometria del cursore. Apple documenta che accedere a questa proprietà su una vista del testo o su un contenitore di testo può attivare la modalità di compatibilità, che usa NSLayoutManager su macOS 12 e versioni successive. La chiamata all’API di alto livello per lo scorrimento evita di introdurre questa modifica di compatibilità di TextKit solo per rendere visibile la selezione. Comportamento della compatibilità di TextKit.
Comprendi i limiti di un cursore sempre visibile
Se per “mantenerlo visibile” intendi “impedire che lampeggi”, il comando di scorrimento non soddisfa questo requisito. La documentazione moderna di Apple sui cursori distingue l’animazione automatica dalla modalità di visualizzazione sempre visibile.
Da macOS 14 in poi, NSTextInsertionIndicator offre le modalità di visualizzazione .automatic, .visible e .hidden. Apple documenta .visible per mostrare continuamente un cursore con le animazioni disattivate, per esempio quando si indica la destinazione di un trascinamento. Le viste di testo AppKit standard usano automaticamente l’interfaccia di sistema per il cursore. Guida all’adozione del cursore di testo di sistema, NSTextInsertionIndicator.
Il percorso documentato per i cursori personalizzati richiede di creare e posizionare un indicatore personalizzato. La modalità di visualizzazione controlla quella specifica istanza; la guida di Apple non espone un’impostazione su NSTextView per portare su .visible l’indicatore gestito internamente. Considera il percorso personalizzato una scelta architetturale per interfacce di testo personalizzate, che comporta la responsabilità di aggiornare la geometria e lo stato attivo del cursore. Non è una modifica direttamente applicabile a questo wrapper.
Due API dall’aspetto meno recente non forniscono l’impostazione mancante. shouldDrawInsertionPoint è documentata come di sola lettura. drawInsertionPoint(in:color:turnedOn:) disegna o cancella il cursore e Apple afferma che chi la chiama non dovrebbe aver bisogno di invocarla direttamente. Nessuna delle due è documentata come opzione per mantenerlo sempre visibile. Stato del disegno del punto di inserimento, metodo di disegno del punto di inserimento.
Per la normale attività di modifica descritta in questo articolo, mantieni l’animazione del cursore di AppKit e offri un modo affidabile per tornare alla posizione corrente. Un’implementazione con cursore personalizzato richiede una progettazione dedicata e, se usa NSTextInsertionIndicator, un controllo di disponibilità per macOS 14.
Verifica il comportamento nella tua app
Usa l’esempio per controllare separatamente lo scorrimento, la selezione, il focus e l’aspetto. I comportamenti seguenti sono quelli previsti in base all’implementazione, non risultati di un’esecuzione riportata.
- Avvia l’app e premi
Rivela punto di inserimento. L’editor dovrebbe ricevere il focus e rendere visibile la posizione finale. - Fai clic su una riga precedente per spostare il cursore. Scorri altrove senza fare clic su un altro punto del testo, quindi premi il pulsante. Dovrebbe rendere visibile la posizione precedente.
- Seleziona alcune parole e premi il pulsante. La selezione dovrebbe rimanere tale; il comando non la riduce a un cursore.
- Digita un’etichetta facilmente riconoscibile, ad esempio
triangolo arancione, quindi premi più volte il pulsante. Il contenuto modificato dovrebbe rimanere intatto. - Ridimensiona la finestra e ripeti l’operazione di visualizzazione. Il comando dovrebbe continuare a usare l’intervallo corrente anziché una vecchia coordinata dello schermo.
- Controlla le modalità chiara e scura nelle versioni di macOS supportate. Verifica che testo, sfondo e colori del cursore siano leggibili insieme.
Se lo scorrimento funziona ma il cursore continua a comparire a intermittenza, verifica se stai osservando il normale lampeggiamento o una selezione di testo attiva. Se il comando non fa nulla, controlla prima che l’editor sia associato a una finestra e i controlli sul primo responder. Intervieni sul passaggio che non funziona: conserva il testo, ripristina il focus quando richiesto e rendi visibile l’intervallo corrente.
Fonti
- NSViewRepresentable — Apple
- NSTextView — Apple
- scrollableTextView() — Apple
- scrollRangeToVisible(_:) — Apple
- makeFirstResponder(_:) — Apple
- length — Apple
- String.UTF16View — Apple
- count — Apple
- insertionPointColor — Apple
- textColor — Apple
- textBackgroundColor — Apple
- Adopting the system text cursor in custom text views — Apple
- NSTextInsertionIndicator — Apple
- shouldDrawInsertionPoint — Apple
- drawInsertionPoint(in:color:turnedOn:) — Apple