Um die Einfügemarke in einem von Ihrer Mac-App umschlossenen NSTextView anzuzeigen, stellen Sie den Tastaturfokus wieder her und rufen Sie scrollRangeToVisible(_:) mit der aktuellen Auswahl auf. Konfigurieren Sie die Farbe der Einfügemarke separat. Die Einfügemarke auf dem Bildschirm zu halten und ihr Blinken zu unterbinden, erfordern unterschiedliche Steuerelemente.
Diese Anleitung erstellt einen kleinen lokalen Editor mit einer Schaltfläche Einfügemarke anzeigen. Sie behandelt eine vertraute Aufgabe am Desktop: nach dem Scrollen an eine andere Stelle im Dokument zur Bearbeitungsposition zurückzukehren. Die Implementierung behält die aktuelle Auswahl bei und überlässt AppKit die normale Texteingabe und Cursorzeichnung.
Ermitteln, warum die Einfügemarke fehlt
Beginnen Sie mit dem sichtbaren Symptom. Scrollen, Fokus, Auswahl und Cursoranimation können ähnlich aussehen, erfordern aber jeweils eine andere Reaktion.
| Ihre Beobachtung | Was zu prüfen ist | Geeignete Maßnahme |
|---|---|---|
| Ihre Bearbeitungsposition liegt außerhalb des sichtbaren Dokumentbereichs | Die aktuelle Auswahl der Textansicht | Diesen Bereich in den sichtbaren Bereich scrollen |
| Durch Klicken auf ein anderes Steuerelement endet die Texteingabe im Editor | Der First Responder des Fensters | Den Fokus auf ausdrückliche Anfrage zurückgeben |
| Die Einfügemarke hebt sich kaum vom Hintergrund ab | Farben der Einfügemarke und des Hintergrunds | Die Farbeigenschaften der Textansicht konfigurieren |
| Statt einer einzelnen Einfügemarke ist Text hervorgehoben | Ob der ausgewählte Bereich eine Länge größer null hat | Die Auswahl beibehalten, sofern der Benutzer keine Verschiebung anfordert |
| Die Einfügemarke verschwindet und erscheint wieder, während der Editor aktiv ist | Normale Cursoranimation | Das Blinken getrennt vom Scrollen behandeln |
Apple dokumentiert scrollRangeToVisible(_:) als Methode, die eine umschließende Scrollansicht scrollt, bis die ersten Zeichen des angegebenen Bereichs sichtbar sind. Sie verspricht weder, diesen Bereich zu zentrieren, noch eine lange Auswahl vollständig anzuzeigen. Damit eignet sie sich dazu, eine Einfügeposition sichtbar zu machen, ohne Glyphenrechtecke selbst berechnen zu müssen. Apples Dokumentation zum Scrollen.
Der Fokus gehört zum umschließenden Fenster. Apple weist ausdrücklich an, window.makeFirstResponder(textView) zu verwenden, statt becomeFirstResponder() der Textansicht direkt aufzurufen. Die folgende Implementierung geht diesen Weg und prüft, welches Objekt tatsächlich zum First Responder geworden ist. Apples Dokumentation zu NSTextView, Dokumentation zum Wechsel des First Responders.
Einen lokalen Editor mit einem Anzeigebefehl erstellen
Erstellen Sie in Xcode ein macOS-App-Projekt mit SwiftUI und Swift. Legen Sie für dieses Beispiel macOS 13 oder neuer als Bereitstellungsziel fest. Ersetzen Sie den generierten App-Einstiegspunkt durch die erste Datei unten und fügen Sie die zweite Datei demselben App-Ziel hinzu. Zusammen bilden diese Dateien die vollständige Beispiel-App.
Das Beispiel verwendet synthetischen Text und behält Änderungen für die Lebensdauer der AppKit-Ansicht in dieser Ansicht bei. Es enthält weder eine Implementierung zum Speichern von Dokumenten noch ein externes Textmodell. Beim Schließen der App gehen Änderungen verloren. Der Code wurde anhand der abgerufenen Dokumentation geprüft, hier aber nicht kompiliert oder ausgeführt, da die verfügbare Umgebung Linux ist und kein AppKit-SDK enthält.
Das SwiftUI-Fenster erstellen
Die Schaltfläche erhöht eine Anforderungsnummer. Dadurch erhält die Umschließung einen konkreten Befehl zur Verarbeitung, anstatt bei jeder SwiftUI-Aktualisierung den Fokus des Editors wiederherzustellen oder zu scrollen.
// 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)
}
}
Den scrollbaren AppKit-Editor umschließen
NSViewRepresentable ist Apples Brücke zur Verwaltung einer AppKit-Ansicht in einer SwiftUI-Hierarchie. Hier ist die dargestellte Ansicht eine NSScrollView mit einem NSTextView als Dokumentansicht. Der Koordinator merkt sich die zuletzt verarbeitete Anforderung. Apple stellt die Factory für scrollbare Textansichten bereit, die bei der Erstellung verwendet wird. 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())
}
}
Der Editor beginnt mit einer Auswahl der Länge null am Ende seines Beispieltexts. Durch Drücken von Einfügemarke anzeigen erhält der Editor den Fokus und diese Position wird sichtbar. Bei späteren Betätigungen wird die aktuelle Auswahl verwendet, sodass sich die vom Befehl angezeigte Position ändert, wenn Sie die Einfügemarke an eine andere Stelle setzen. Der Befehl ersetzt keinen Text und verschiebt die Auswahl nicht.
Für die Endposition wird NSString.length verwendet, das UTF-16-Codeeinheiten zählt. Die UTF-16-Ansicht von Swift entspricht den indizierten NSString-APIs; String.count von Swift zählt hingegen Zeichen. Halten Sie diese Darstellungen konsistent, wenn Sie AppKit-Textbereiche erstellen. Das Lesen eines vorhandenen selectedRange() erspart es, für den Anzeigebefehl selbst einen Offset zu berechnen. NSString length, Swift String.UTF16View, Swift String.count.
Bearbeitungsstatus bei SwiftUI-Aktualisierungen beibehalten
Die entscheidende Designwahl ist die Aufteilung zwischen Erstellung und Befehlen. makeNSView initialisiert den Editor einmal. updateNSView reagiert auf eine geänderte Anforderungsnummer und lässt den Dokumentinhalt unangetastet. Dies entspricht dem von Apple beschriebenen Representable-Lebenszyklus zum Erstellen und Aktualisieren von AppKit-Ansichten. SwiftUI steuert außerdem das Layout der dargestellten Ansicht, daher weist diese Umschließung der Scrollansicht weder einen Frame noch Bounds zu. Apples Hinweise zum Representable-Lebenszyklus und Layout.
Vermeiden Sie in diesem Beispiel, textView.string = initialText bei jeder Aktualisierung zuzuweisen. Dadurch würde das aktuelle Dokument des Benutzers durch den ursprünglichen Beispieltext ersetzt. Ebenso würde ein bedingungsloses Setzen der Auswahl ans Dokumentende dazu führen, dass ein Anzeigebefehl die Bearbeitungsposition verschiebt.
Wenn ein Editor durch SwiftUI-Zustand gestützt wird, fügen Sie eine gezielte Synchronisierung in beide Richtungen hinzu: Leiten Sie Benutzeränderungen über einen Koordinator weiter und legen Sie fest, wann eingehende Modelländerungen den Text ersetzen. Behalten Sie gültige Auswahlen bei solchen Ersetzungen bei und definieren Sie, wie gleichzeitige Änderungen aufgelöst werden. Das ist eine andere Aufgabe als das Anzeigen der Einfügemarke; das Beispiel behält absichtlich eine einzige Quelle für den bearbeitbaren Inhalt bei. Apple dokumentiert Koordinatoren als Mechanismus, um Interaktionen über Delegates und Target-Action an SwiftUI weiterzuleiten. Hinweise zu Koordinatoren.
Auch die Fokusprüfungen sind wichtig. makeFirstResponder(_:) kann fehlschlagen, wenn der vorhandene Responder sich weigert, den Fokus abzugeben. Apple weist außerdem darauf hin, dass bei erfolgreicher Rückgabe das Fenster selbst zum First Responder werden kann, wenn der angeforderte Responder den Fokus nicht annimmt. Durch Prüfen von window.firstResponder === textView wird das tatsächliche Ergebnis vor dem Scrollen verifiziert. Rückgabewerte und Verhalten beim Wechsel des First Responders.
Ist der Editor keinem Fenster zugeordnet, verarbeitet dieses Beispiel die Anforderung, ohne etwas zu tun. Drücken Sie die Schaltfläche erneut, sobald er zugeordnet ist. Entwerfen Sie für eine Funktion zum anfänglichen Fokussieren eine explizite, an die Fensterzuordnung gebundene Anforderung, anstatt den Fokus bei sachfremden Aktualisierungen wiederholt zurückzufordern.
Farbe konfigurieren, ohne die Cursorzeichnung zu ersetzen
Das Beispiel weist .textColor sowohl dem Text als auch der Einfügemarke und .textBackgroundColor dem Editorhintergrund zu. Dies sind dokumentierte semantische Farben für den Text und den Bereich dahinter. insertionPointColor ist die dokumentierte Eigenschaft zum Festlegen der Farbe der Einfügemarke. Prüfen Sie das Ergebnis unter jeder von Ihrer App unterstützten macOS-Version und Darstellung. Farbe der Einfügemarke, Textfarbe, Texthintergrundfarbe.
Die Farbkonfiguration fordert weder den Fokus an noch scrollt sie das Dokument oder wählt einen Modus ohne Blinken. Halten Sie diese Aufgaben bei der Diagnose einer fehlenden Einfügemarke getrennt. Eine Farbänderung kann beispielsweise keine Position sichtbar machen, die weiterhin außerhalb des sichtbaren Bereichs der Scrollansicht liegt.
Das Beispiel vermeidet außerdem den Zugriff auf layoutManager, um die Geometrie der Einfügemarke zu berechnen. Apple dokumentiert, dass der Zugriff auf diese Eigenschaft einer Textansicht oder eines Textcontainers unter macOS 12 und neuer die Ansicht in einen Kompatibilitätsmodus mit NSLayoutManager versetzen kann. Der Aufruf der höherstufigen Scrollmethode vermeidet diese Änderung der TextKit-Kompatibilität, nur um die Auswahl sichtbar zu machen. Verhalten des TextKit-Kompatibilitätsmodus.
Die Grenzen einer immer sichtbaren Einfügemarke verstehen
Wenn „sichtbar halten“ „niemals blinken“ bedeutet, erfüllt der Scrollbefehl diese Anforderung nicht. Apples moderne Cursor-Dokumentation unterscheidet zwischen automatischer Animation und einem Anzeigemodus, in dem der Cursor immer sichtbar ist.
Unter macOS 14 und neuer bietet NSTextInsertionIndicator die Anzeigemodi .automatic, .visible und .hidden. Apple dokumentiert .visible, um einen Cursor dauerhaft und ohne Animationen anzuzeigen, etwa als Hinweis auf ein Drag-and-drop-Ziel. Standardmäßige AppKit-Textansichten verwenden automatisch die System-Cursoroberfläche. Leitfaden zur Verwendung des Systemtextcursors, NSTextInsertionIndicator.
Der dokumentierte Weg für benutzerdefinierte Cursor erfordert, eine eigene Anzeige zu erstellen und zu positionieren. Der Anzeigemodus steuert diese Instanz; Apples Leitfaden bietet keine Einstellung auf NSTextView, um dessen intern verwaltete Anzeige auf .visible zu setzen. Betrachten Sie den benutzerdefinierten Ansatz als Architekturentscheidung für eine eigene Textoberfläche, die auch die Verantwortung für die Aktualisierung der Cursorgeometrie und des Aktivierungszustands umfasst. Er lässt sich nicht direkt in diese Umschließung übernehmen.
Zwei älter wirkende APIs bieten diese fehlende Einstellung nicht. shouldDrawInsertionPoint ist als schreibgeschützt dokumentiert. drawInsertionPoint(in:color:turnedOn:) zeichnet den Cursor oder löscht seine Darstellung, und Apple sagt, dass Aufrufer die Methode nicht direkt aufrufen müssen sollten. Keine der beiden APIs ist als Schalter für dauerhafte Sichtbarkeit dokumentiert. Zeichnungsstatus der Einfügemarke, Zeichenmethode für die Einfügemarke.
Behalten Sie für die gewöhnliche Editoraufgabe in diesem Artikel AppKits Cursoranimation bei und bieten Sie eine zuverlässige Möglichkeit, zur aktuellen Position zurückzukehren. Eine benutzerdefinierte Cursorimplementierung benötigt ein eigenes Design und bei Verwendung von NSTextInsertionIndicator eine Verfügbarkeitsgrenze bei macOS 14.
Das Verhalten in Ihrer App überprüfen
Verwenden Sie das Beispiel, um Prüfungen für Scrollen, Auswahl, Fokus und Darstellung voneinander zu trennen. Im Folgenden stehen erwartete Verhaltensweisen auf Grundlage der Implementierung, keine berichteten Ausführungsergebnisse.
- Starten Sie die App und drücken Sie
Einfügemarke anzeigen. Der Editor sollte den Fokus erhalten und seine Endposition sichtbar machen. - Klicken Sie in eine frühere Zeile, um die Einfügemarke zu verschieben. Scrollen Sie weg, ohne auf eine andere Textposition zu klicken, und drücken Sie dann die Schaltfläche. Die frühere Position sollte sichtbar werden.
- Markieren Sie mehrere Wörter und drücken Sie die Schaltfläche. Die Auswahl sollte markiert bleiben; der Befehl reduziert sie nicht auf eine einzelne Einfügemarke.
- Geben Sie eine unverwechselbare Bezeichnung wie
orange triangleein und drücken Sie die Schaltfläche mehrmals. Der bearbeitete Inhalt sollte erhalten bleiben. - Ändern Sie die Fenstergröße und wiederholen Sie den Anzeigebefehl. Er sollte weiterhin den aktuellen Bereich statt einer alten Bildschirmkoordinate verwenden.
- Prüfen Sie sowohl die helle als auch die dunkle Darstellung unter den unterstützten macOS-Versionen. Vergewissern Sie sich, dass Text-, Hintergrund- und Einfügemarkenfarben zusammen gut lesbar bleiben.
Wenn das Scrollen funktioniert, die Einfügemarke aber weiterhin zeitweise erscheint, prüfen Sie, ob Sie normales Blinken oder eine aktive Textauswahl beobachten. Wenn der Befehl nichts bewirkt, untersuchen Sie zuerst die Prüfungen für Fensterzuordnung und First Responder. Richten Sie die Fehlerbehebung auf den betroffenen Schritt aus: Text beibehalten, den Fokus auf Anfrage wiederherstellen und den aktuellen Bereich sichtbar machen.
Quellen
- 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