Aby ponownie wyświetlić punkt wstawiania w opakowanym obiekcie NSTextView w aplikacji na Maca, przywróć fokus klawiatury i wywołaj scrollRangeToVisible(_:) z bieżącym zaznaczeniem. Kolor kursora skonfiguruj osobno. Utrzymywanie kursora na ekranie i wyłączenie jego migania wymaga użycia różnych mechanizmów.
W tym przewodniku utworzysz niewielki lokalny edytor z przyciskiem Reveal insertion point. Rozwiązuje on typowe zadanie na komputerze: powrót do miejsca edycji po przewinięciu dokumentu w inne miejsce. Implementacja zachowuje bieżące zaznaczenie i pozwala AppKit obsługiwać standardowe wprowadzanie tekstu oraz rysowanie kursora.
Ustal, dlaczego punkt wstawiania jest niewidoczny
Zacznij od widocznych objawów. Przewijanie, fokus, zaznaczenie i animacja kursora mogą dawać podobne wrażenie, ale każde z nich wymaga innego działania.
| Co widzisz | Co sprawdzić | Właściwe działanie |
|---|---|---|
| Miejsce edycji znajduje się poza widocznym obszarem dokumentu | Bieżące zaznaczenie widoku tekstu | Przewiń do tego zakresu |
| Po kliknięciu innego elementu przestajesz pisać w edytorze | Pierwszy responder okna | Na żądanie jawnie przywróć fokus |
| Kursor trudno odróżnić od tła | Kolory kursora i tła | Skonfiguruj właściwości kolorów widoku tekstu |
| Zamiast pojedynczego kursora widoczny jest podświetlony tekst | Czy zaznaczony zakres ma niezerową długość | Zachowaj zaznaczenie, chyba że użytkownik poprosi o jego przesunięcie |
| Kursor znika i pojawia się ponownie, gdy edytor jest aktywny | Standardowa animacja kursora | Rozpatruj miganie osobno od przewijania |
Apple opisuje scrollRangeToVisible(_:) jako przewijanie otaczającego widoku przewijania do chwili, gdy widoczne staną się pierwsze znaki podanego zakresu. Nie gwarantuje wyśrodkowania zakresu ani wyświetlenia całego długiego zaznaczenia. Dzięki temu metoda nadaje się do ponownego wyświetlania miejsca wstawiania bez samodzielnego obliczania prostokątów znaków. Dokumentacja Apple dotycząca przewijania.
Fokus należy do okna zawierającego widok. Apple wyraźnie zaleca wywoływanie window.makeFirstResponder(textView), a nie bezpośrednie wywoływanie becomeFirstResponder() widoku tekstu. Poniższa implementacja postępuje zgodnie z tym zaleceniem i sprawdza, który obiekt faktycznie stał się pierwszym responderem. Dokumentacja Apple dotycząca NSTextView, dokumentacja przejścia na pierwszego respondera.
Utwórz lokalny edytor z poleceniem ponownego wyświetlania
W Xcode utwórz projekt aplikacji macOS używający SwiftUI i Swift. W tym przykładzie ustaw docelową wersję systemu na macOS 13 lub nowszą. Zastąp wygenerowany punkt wejścia aplikacji pierwszym plikiem poniżej i dodaj drugi plik do tego samego celu aplikacji. Razem tworzą kompletną aplikację demonstracyjną.
Przykład używa wygenerowanego tekstu i przechowuje edycje w widoku AppKit przez cały czas jego istnienia. Nie zawiera mechanizmu zapisywania dokumentu ani zewnętrznego modelu tekstu. Zamknięcie aplikacji powoduje utratę edycji. Kod porównano z pobraną dokumentacją, ale nie skompilowano go ani nie uruchomiono, ponieważ dostępne środowisko to Linux bez SDK AppKit.
Utwórz okno SwiftUI
Przycisk zwiększa numer żądania. Dzięki temu opakowanie otrzymuje konkretne polecenie do obsłużenia, zamiast przy każdym odświeżeniu SwiftUI ponownie ustawiać fokus lub przewijać edytor.
// 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)
}
}
Owiń przewijany edytor AppKit
NSViewRepresentable to mechanizm Apple służący do obsługi widoku AppKit w hierarchii SwiftUI. W tym przypadku reprezentowanym widokiem jest NSScrollView, którego widokiem dokumentu jest NSTextView. Koordynator zapamiętuje ostatnie obsłużone żądanie. Apple udostępnia fabrykę przewijanego widoku tekstu, używaną podczas tworzenia. 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())
}
}
Edytor rozpoczyna z zaznaczeniem o zerowej długości na końcu przykładowego tekstu. Naciśnięcie Reveal insertion point ustawia fokus na edytorze i wyświetla tę pozycję. Kolejne naciśnięcia korzystają z aktualnego zaznaczenia, więc po przesunięciu kursora polecenie wyświetli nową pozycję. Polecenie nie zastępuje tekstu ani nie przesuwa zaznaczenia.
Pozycja końcowa korzysta z NSString.length, które zlicza jednostki kodowe UTF-16. Widok UTF-16 w Swift jest zgodny z indeksowanymi API NSString, natomiast String.count w Swift zlicza znaki. Podczas tworzenia zakresów tekstu AppKit używaj spójnych reprezentacji. Odczyt istniejącego selectedRange() pozwala uniknąć obliczania przesunięcia na potrzeby samego polecenia wyświetlenia pozycji. NSString length, Swift String.UTF16View, Swift String.count.
Zachowaj stan edycji podczas aktualizacji SwiftUI
Kluczowym wyborem projektowym jest rozdzielenie tworzenia od obsługi poleceń. makeNSView wypełnia edytor początkową treścią tylko raz. updateNSView reaguje na zmianę numeru żądania i nie zmienia zawartości dokumentu. Jest to zgodne z cyklem życia reprezentowalnych widoków opisanym przez Apple dla tworzenia i aktualizowania widoków AppKit. SwiftUI kontroluje też układ reprezentowanego widoku, dlatego to opakowanie nie przypisuje ramki ani granic widoku przewijania. Wskazówki Apple dotyczące cyklu życia reprezentowalnych widoków i układu.
W tym przykładzie unikaj przypisywania textView.string = initialText przy każdej aktualizacji. Spowodowałoby to zastąpienie bieżącego dokumentu użytkownika jego pierwotnym przykładowym tekstem. Podobnie bezwarunkowe ustawianie zaznaczenia na końcu dokumentu sprawiłoby, że polecenie wyświetlenia pozycji przesuwałoby miejsce edycji.
W edytorze opartym na stanie SwiftUI dodaj przemyślaną synchronizację dwukierunkową: przekazuj edycje użytkownika za pośrednictwem koordynatora i określ, kiedy zmiany modelu przychodzące z zewnątrz zastępują tekst. Zachowuj prawidłowe zaznaczenia podczas tych zamian i określ sposób rozstrzygania równoczesnych zmian. To osobne zadanie od wyświetlania kursora; przykład celowo pozostawia edytowalną zawartość pod kontrolą jednego właściciela. Apple opisuje koordynatory jako mechanizm przekazywania do SwiftUI interakcji delegata oraz target-action. Wskazówki dotyczące koordynatora.
Istotne są również warunki sprawdzające fokus. makeFirstResponder(_:) może zakończyć się niepowodzeniem, jeśli istniejący responder odmawia rezygnacji z tej roli. Apple zaznacza też, że pomyślny wynik może pozostawić samo okno jako pierwszego respondera, jeśli wskazany responder nie przyjmie fokusu. Sprawdzenie window.firstResponder === textView pozwala zweryfikować faktyczny wynik przed przewinięciem. Wartości zwrotne i działanie podczas przechodzenia do pierwszego respondera.
Jeśli edytor nie jest dołączony do okna, ten przykład przyjmuje żądanie i nie wykonuje żadnego działania. Po dołączeniu edytora naciśnij przycisk ponownie. Funkcję ustawiania fokusu przy pierwszym wyświetleniu zaprojektuj jako jawne żądanie uwzględniające dołączenie do okna, zamiast wielokrotnie przejmować fokus przy niepowiązanych aktualizacjach.
Skonfiguruj kolor bez zastępowania rysowania kursora
Przykład przypisuje .textColor zarówno tekstowi, jak i punktowi wstawiania, a .textBackgroundColor — tłu edytora. Są to udokumentowane kolory semantyczne tekstu i obszaru za nim. insertionPointColor to udokumentowana właściwość służąca do ustawiania koloru kursora. Sprawdź wygląd w każdej wersji macOS i dla każdego wyglądu obsługiwanego przez aplikację. Kolor kursora, kolor tekstu, kolor tła tekstu.
Konfiguracja kolorów nie ustawia fokusu, nie przewija dokumentu ani nie wybiera trybu wyświetlania bez migania. Podczas diagnozowania niewidocznego kursora traktuj te kwestie osobno. Na przykład zmiana koloru nie wyświetli pozycji pozostającej poza widocznym obszarem widoku przewijania.
Przykład unika też odczytywania layoutManager w celu obliczenia geometrii kursora. Apple informuje, że odczyt tej właściwości w widoku tekstu lub kontenerze tekstowym może przełączyć widok w tryb zgodności korzystający z NSLayoutManager w macOS 12 i nowszych. Wywołanie wysokopoziomowego API przewijania pozwala uniknąć wprowadzania tej zmiany zgodności TextKit tylko po to, by wyświetlić zaznaczenie. Działanie trybu zgodności TextKit.
Poznaj ograniczenia kursora stale widocznego
Jeśli „utrzymuj widoczność” oznacza „nigdy nie migaj”, polecenie przewijania nie spełni tego wymagania. Nowoczesna dokumentacja Apple dotycząca kursora rozróżnia automatyczną animację od trybu stale widocznego.
W macOS 14 i nowszych NSTextInsertionIndicator udostępnia tryby wyświetlania .automatic, .visible i .hidden. Apple opisuje .visible jako tryb ciągłego wyświetlania kursora bez animacji, na przykład w celu wskazania miejsca upuszczenia przeciąganego elementu. Standardowe widoki tekstu AppKit automatycznie korzystają z systemowego interfejsu kursora. Przewodnik po obsłudze systemowego kursora tekstowego, NSTextInsertionIndicator.
Udokumentowana ścieżka własnego kursora wymaga utworzenia i pozycjonowania własnego wskaźnika. Tryb wyświetlania kontroluje tę instancję; przewodnik Apple nie udostępnia w NSTextView ustawienia pozwalającego zmienić tryb wewnętrznie zarządzanego wskaźnika na .visible. Wybór własnego kursora wiąże się z przyjęciem odpowiedzialności za aktualizowanie geometrii kursora i stanu aktywności. Nie jest to bezpośrednia zamiana w tym opakowaniu.
Dwa starsze API nie zapewniają brakującego ustawienia. shouldDrawInsertionPoint jest udokumentowana jako właściwość tylko do odczytu. drawInsertionPoint(in:color:turnedOn:) rysuje lub wymazuje kursor, a Apple zaznacza, że wywoływanie jej bezpośrednio przez użytkownika nie powinno być potrzebne. Żadne z tych API nie jest udokumentowane jako przełącznik stale widocznego kursora. Stan rysowania punktu wstawiania, Metoda rysowania punktu wstawiania.
W typowym zadaniu edycji opisanym w tym artykule pozostaw animację kursora AppKit i zapewnij niezawodny sposób powrotu do bieżącej pozycji. Własna implementacja kursora wymaga osobnego projektu, a w przypadku użycia NSTextInsertionIndicator także uwzględnienia dostępności od macOS 14.
Sprawdź działanie w aplikacji
Użyj przykładu, by osobno sprawdzić przewijanie, zaznaczenie, fokus i wygląd. Poniższe punkty opisują oczekiwane zachowanie wynikające z implementacji, a nie wyniki zgłoszonego uruchomienia.
- Uruchom aplikację i naciśnij
Reveal insertion point. Edytor powinien otrzymać fokus i wyświetlić pozycję na końcu. - Kliknij we wcześniejszym wierszu, aby przesunąć kursor. Przewiń dokument bez klikania innego miejsca tekstu, a następnie naciśnij przycisk. Powinien wyświetlić wcześniejszą pozycję.
- Zaznacz kilka słów i naciśnij przycisk. Zaznaczenie powinno pozostać aktywne; polecenie nie zwija go do kursora.
- Wpisz charakterystyczną etykietę, na przykład
orange triangle, a następnie naciśnij przycisk kilka razy. Edytowana zawartość powinna pozostać bez zmian. - Zmień rozmiar okna i ponownie wykonaj polecenie wyświetlenia pozycji. Powinno nadal korzystać z bieżącego zakresu, a nie ze starych współrzędnych ekranu.
- Sprawdź jasny i ciemny wygląd w obsługiwanych wersjach macOS. Upewnij się, że tekst, tło i kolor kursora są ze sobą czytelne.
Jeśli przewijanie działa, ale kursor nadal okresowo znika, sprawdź, czy obserwujesz standardowe miganie lub aktywne zaznaczenie tekstu. Jeśli polecenie nic nie robi, najpierw sprawdź warunki dotyczące dołączenia widoku do okna i pierwszego respondera. Powiąż poprawkę z elementem, który zawodzi: zachowaj tekst, na żądanie przywróć fokus i wyświetl bieżący zakres.
Źródła
- NSViewRepresentable — Apple
- NSTextView — Apple
- scrollableTextView() — Apple
- scrollRangeToVisible(_:) — Apple
- makeFirstResponder(_:) — Apple
- length — Apple
- String.UTF16View — Apple
- count — Apple
- insertionPointColor — Apple
- textColor — Apple
- textBackgroundColor — Apple
- Przyjmowanie systemowego kursora tekstowego we własnych widokach tekstu — Apple
- NSTextInsertionIndicator — Apple
- shouldDrawInsertionPoint — Apple
- drawInsertionPoint(in:color:turnedOn:) — Apple