Om het invoegpunt in de met een wrapper omgeven NSTextView van je Mac-app zichtbaar te maken, herstel je de toetsenbordfocus en roep je scrollRangeToVisible(_:) aan met de huidige selectie. Stel de kleur van de caret afzonderlijk in. De caret op het scherm houden en laten stoppen met knipperen vereisen verschillende instellingen.
Deze gids bouwt een kleine lokale editor met een knop Invoegpunt tonen. De gids behandelt een bekende desktoptaak: terugkeren naar je bewerkingspositie nadat je ergens anders in een document hebt gescrold. De implementatie behoudt de huidige selectie en laat AppKit de normale tekstinvoer en het tekenen van de cursor beheren.
Achterhalen waarom het invoegpunt ontbreekt
Begin met wat je ziet. Scrollen, focus, selectie en cursoranimatie kunnen vergelijkbare resultaten opleveren, maar elk vereist een andere aanpak.
| Wat je ziet | Wat je moet controleren | Geschikte actie |
|---|---|---|
| Je bewerkingspositie bevindt zich buiten het zichtbare documentgebied | De huidige selectie van de tekstweergave | Scroll dat bereik in beeld |
| Als je op een andere knop klikt, kun je niet meer in de editor typen | De first responder van het venster | Herstel de focus expliciet wanneer daarom wordt gevraagd |
| De caret is moeilijk van de achtergrond te onderscheiden | De kleuren van de caret en de achtergrond | Stel de kleureigenschappen van de tekstweergave in |
| Tekst is gemarkeerd in plaats van dat er één caret wordt weergegeven | Of het geselecteerde bereik een lengte groter dan nul heeft | Behoud de selectie, tenzij de gebruiker om een verplaatsing vraagt |
| De caret verdwijnt en verschijnt weer terwijl de editor actief is | Normale cursoranimatie | Behandel knipperen los van scrollen |
Apple documenteert scrollRangeToVisible(_:) als een functie die een omvattende scrollweergave verplaatst totdat de eerste tekens van het opgegeven bereik zichtbaar zijn. De functie belooft niet dat het bereik wordt gecentreerd of dat een volledige lange selectie wordt weergegeven. Daarmee is de functie geschikt om een invoegpositie zichtbaar te maken zonder zelf glyph-rechthoeken te berekenen. Apples documentatie over scrollen.
De focus hoort bij het omvattende venster. Apple instrueert aanroepers expliciet om window.makeFirstResponder(textView) te gebruiken in plaats van rechtstreeks becomeFirstResponder() van de tekstweergave aan te roepen. De implementatie hieronder volgt die aanpak en controleert welk object daadwerkelijk first responder is geworden. Apples documentatie over NSTextView, documentatie over de overgang van first responder.
Een lokale editor met een opdracht om het invoegpunt te tonen bouwen
Maak in Xcode een macOS-appproject met SwiftUI en Swift. Stel voor dit voorbeeld het minimumdoel in op macOS 13 of nieuwer. Vervang het gegenereerde startpunt van de app door het eerste bestand hieronder en voeg het tweede bestand toe aan hetzelfde appdoel. Samen vormen deze bestanden de volledige demonstratie-app.
Het voorbeeld gebruikt synthetische tekst en bewaart bewerkingen in de AppKit-weergave zolang die weergave bestaat. Er is geen implementatie voor het opslaan van documenten en geen extern tekstmodel. Als je de app sluit, gaan bewerkingen verloren. De code is vergeleken met de opgehaalde documentatie; de code is hier niet gecompileerd of uitgevoerd, omdat de beschikbare omgeving Linux is en geen AppKit-SDK heeft.
Het SwiftUI-venster maken
De knop verhoogt een aanvraagn nummer. Zo krijgt de wrapper een specifieke opdracht om uit te voeren, in plaats van dat elke SwiftUI-update de editor opnieuw focust of laat 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)
}
}
De scrollbare AppKit-editor inpakken
NSViewRepresentable is Apples brug om een AppKit-weergave in een SwiftUI-hiërarchie te beheren. Hier is de weergegeven weergave een NSScrollView, met een NSTextView als documentweergave. De coordinator onthoudt het laatst verwerkte verzoek. Apple levert de fabrieksmethode voor de scrollbare tekstweergave die tijdens het maken wordt gebruikt. 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())
}
}
De editor begint met een selectie van lengte nul aan het einde van de voorbeeldtekst. Als je op Invoegpunt tonen drukt, krijgt de editor de focus en wordt die positie zichtbaar. Bij volgende klikken wordt de actuele selectie gebruikt, zodat het verplaatsen van de caret de positie wijzigt die de opdracht toont. De opdracht vervangt geen tekst en verplaatst de selectie niet.
De eindpositie gebruikt NSString.length, dat UTF-16-code-eenheden telt. De UTF-16-weergave van Swift komt overeen met de geïndexeerde NSString-API's; Swift's String.count telt in plaats daarvan tekens. Houd deze representaties consistent wanneer je tekstbereiken voor AppKit maakt. Een bestaande selectedRange() uitlezen voorkomt dat je voor de opdracht om de positie te tonen zelf een offset moet berekenen. NSString length, Swift String.UTF16View, Swift String.count.
Bewerkingsstatus behouden tijdens SwiftUI-updates
De cruciale ontwerpkeuze is de scheiding tussen aanmaken en opdrachten. makeNSView vult de editor eenmalig. updateNSView reageert wanneer een aanvraagn nummer verandert en laat de documentinhoud ongemoeid. Dit volgt de levenscyclus van representables die Apple beschrijft voor het aanmaken en bijwerken van AppKit-weergaven. SwiftUI beheert ook de lay-out van de weergegeven weergave, dus deze wrapper stelt de frame- of bounds-eigenschappen van de scrollweergave niet in. Apples richtlijnen voor de levenscyclus en lay-out van representables.
Wijs in dit voorbeeld niet bij elke update textView.string = initialText toe. Daarmee zou het huidige document van de gebruiker worden vervangen door het oorspronkelijke voorbeeld. Evenzo zou het onvoorwaardelijk instellen van de selectie op het einde van het document ervoor zorgen dat een opdracht om het invoegpunt te tonen de bewerkingspositie verplaatst.
Voeg voor een editor die op SwiftUI-status is gebaseerd, bewuste tweerichtingssynchronisatie toe: geef gebruikersbewerkingen via een coordinator door en bepaal wanneer inkomende modelwijzigingen de tekst vervangen. Behoud geldige selecties bij zulke vervangingen en bepaal hoe gelijktijdige wijzigingen worden afgehandeld. Dat is een aparte verantwoordelijkheid naast het tonen van de caret; het voorbeeld houdt de bewerkbare inhoud bewust bij één eigenaar. Apple documenteert coordinators als het mechanisme om interacties via delegates en target-action aan SwiftUI door te geven. Richtlijnen voor coordinators.
Ook de focuscontroles zijn van belang. makeFirstResponder(_:) kan mislukken als de huidige responder weigert de status op te geven. Apple merkt bovendien op dat een succesvolle retourwaarde het venster zelf als first responder kan achterlaten als de gevraagde responder de focus niet accepteert. Door window.firstResponder === textView te controleren, verifieer je het daadwerkelijke resultaat voordat je gaat scrollen. Retourwaarden en gedrag bij het overdragen van first responder.
Als de editor niet aan een venster is gekoppeld, verwerkt dit voorbeeld het verzoek zonder iets te doen. Druk opnieuw op de knop nadat de editor is gekoppeld. Ontwerp voor een functie die bij het starten de focus instelt een expliciet verzoek dat rekening houdt met de koppeling, in plaats van tijdens niet-gerelateerde updates herhaaldelijk de focus terug te nemen.
De kleur instellen zonder de cursorweergave te vervangen
Het voorbeeld kent .textColor toe aan zowel de tekst als het invoegpunt, en .textBackgroundColor aan de editorachtergrond. Dit zijn gedocumenteerde semantische kleuren voor tekst en het gebied erachter. insertionPointColor is de gedocumenteerde eigenschap om de caret-kleur in te stellen. Controleer het resultaat op elke macOS-versie en in elk uiterlijk die je app ondersteunt. Caret-kleur, tekstkleur, tekstachtergrondkleur.
Kleurinstellingen vragen niet om focus, scrollen het document niet en kiezen geen weergavemodus zonder knipperen. Houd die verantwoordelijkheden gescheiden wanneer je een ontbrekende caret onderzoekt. Zo kan een kleurwijziging een positie die buiten het zichtbare gebied van de scrollweergave blijft niet zichtbaar maken.
Het voorbeeld vermijdt ook toegang tot layoutManager om de geometrie van de caret te berekenen. Apple documenteert dat toegang tot deze eigenschap op een tekstweergave of tekstcontainer de weergave op macOS 12 en nieuwer naar een compatibiliteitsmodus kan schakelen die NSLayoutManager gebruikt. De scroll-API op hoog niveau aanroepen voorkomt dat deze wijziging in TextKit-compatibiliteit wordt geïntroduceerd alleen om de selectie zichtbaar te maken. Compatibiliteitsgedrag van TextKit.
Begrijpen wat de grens is van een altijd zichtbare caret
Als “zichtbaar houden” betekent “nooit knipperen”, voldoet de scrollopdracht niet aan die eis. Apples moderne cursordocumentatie maakt onderscheid tussen automatische animatie en een weergavemodus waarin de cursor altijd zichtbaar is.
Op macOS 14 en nieuwer biedt NSTextInsertionIndicator de weergavemodi .automatic, .visible en .hidden. Apple documenteert .visible om een cursor continu te tonen met uitgeschakelde animaties, bijvoorbeeld als aanduiding van een sleepbestemming. Standaard AppKit-tekstweergaven gebruiken automatisch de systeemcursorinterface. Handleiding voor het gebruik van de systeemtekstcursor, NSTextInsertionIndicator.
De gedocumenteerde aanpak voor een aangepaste cursor vereist dat je zelf een indicator aanmaakt en positioneert. De weergavemodus bepaalt het gedrag van die instantie; Apples handleiding biedt geen instelling op NSTextView om de intern beheerde indicator op .visible in te stellen. Beschouw de aangepaste aanpak als een architecturale keuze voor een aangepaste tekstinterface, waarbij je verantwoordelijk bent voor het bijwerken van de cursorgeometrie en de actieve status. Het is geen directe wijziging van deze wrapper.
Twee API's die ouder ogen, bieden die ontbrekende instelling niet. shouldDrawInsertionPoint is alleen-lezen gedocumenteerd. drawInsertionPoint(in:color:turnedOn:) tekent of wist de cursor, en Apple zegt dat aanroepers deze niet rechtstreeks hoeven aan te roepen. Geen van beide is gedocumenteerd als een schakelaar om de cursor altijd zichtbaar te maken. Tekenstatus van het invoegpunt, Tekenen van het invoegpunt.
Behoud voor de gebruikelijke editortaak in dit artikel de cursoranimatie van AppKit en bied een betrouwbare manier om terug te keren naar de huidige positie. Een aangepaste cursorimplementatie vereist een eigen ontwerp en, bij gebruik van NSTextInsertionIndicator, een beschikbaarheidsgrens vanaf macOS 14.
Het gedrag in je app controleren
Gebruik het voorbeeld om controles voor scrollen, selectie, focus en uiterlijk van elkaar te scheiden. Hieronder staan verwachte gedragingen op basis van de implementatie, geen gerapporteerde uitvoeringsresultaten.
- Start de app en druk op
Invoegpunt tonen. De editor hoort de focus te krijgen en de eindpositie te tonen. - Klik in een eerdere rij om de caret te verplaatsen. Scroll weg zonder op een andere tekstpositie te klikken en druk daarna op de knop. De eerdere positie hoort zichtbaar te worden.
- Selecteer meerdere woorden en druk op de knop. De selectie hoort geselecteerd te blijven; de opdracht vouwt deze niet samen tot een caret.
- Typ een herkenbaar label zoals
oranje driehoeken druk enkele keren op de knop. De bewerkte inhoud hoort intact te blijven. - Wijzig het formaat van het venster en herhaal de opdracht om de positie te tonen. De opdracht hoort nog steeds het huidige bereik te gebruiken, geen oude schermcoördinaat.
- Controleer zowel lichte als donkere weergaven op ondersteunde macOS-versies. Controleer of tekst-, achtergrond- en caret-kleuren samen goed leesbaar blijven.
Als scrollen lukt maar de caret nog steeds af en toe verschijnt, controleer dan of je normaal knipperen of een actieve tekstselectie ziet. Als de opdracht niets doet, controleer dan eerst de controles op vensterkoppeling en first responder. Laat de oplossing aansluiten op de stap die misgaat: behoud de tekst, herstel de focus wanneer daarom wordt gevraagd en toon het huidige bereik.
Bronnen
- NSViewRepresentable — Apple
- NSTextView — Apple
- scrollableTextView() — Apple
- scrollRangeToVisible(_:) — Apple
- makeFirstResponder(_:) — Apple
- length — Apple
- String.UTF16View — Apple
- count — Apple
- insertionPointColor — Apple
- textColor — Apple
- textBackgroundColor — Apple
- De systeemtekstcursor gebruiken in aangepaste tekstweergaven — Apple
- NSTextInsertionIndicator — Apple
- shouldDrawInsertionPoint — Apple
- drawInsertionPoint(in:color:turnedOn:) — Apple