Aby programowo utworzony NSTextView przyjmował wpisywany tekst, włącz edycję, dołącz go do okna i wywołaj na tym oknie makeFirstResponder(_:). Okno musi też móc stać się oknem aktywnym. Oto kompletny przykład i ukierunkowana sekwencja diagnostyczna.
Co musi być spełnione, zanim zacznie działać wpisywanie?
Ten poradnik dotyczy jednego zadania: utworzenia małego okna aplikacji Mac, w którym można wpisywać i edytować tekst wielowierszowy. Wykorzystuje bezpośrednio AppKit, a następnie wyjaśnia odpowiadające temu decyzje dotyczące cyklu życia, gdy edytor jest osadzony w SwiftUI.
Znaczenie mają trzy odrębne warunki. Widok tekstu musi zezwalać na edycję, okno musi odbierać zdarzenia klawiatury, a widok tekstu musi być zamierzonym pierwszym responderem w tym oknie. Apple określa keyWindow aplikacji jako okno, które aktualnie odbiera zdarzenia klawiatury; samo wyświetlenie okna nie określa, który widok powinien edytować tekst. Apple: keyWindow
Ustawienie isEditable = true zezwala na edycję, a także umożliwia zaznaczanie tekstu w widoku. Apple dokumentuje, że widoki tekstu są domyślnie edytowalne, ale jawne ustawienie tej właściwości wyraźnie pokazuje konfigurację przykładu. Sama możliwość edycji nie wybiera miejsca docelowego klawiatury. Apple: isEditable
Aby ustawić fokus, użyj makeFirstResponder(textView) należącego do zawierającego widok okna. Apple wyraźnie zaleca deweloperom używanie tej metody zamiast bezpośredniego wywoływania becomeFirstResponder() widoku tekstu. Dzięki temu okno może koordynować przejście między responderami. Apple: NSTextView
Utwórz kompletne edytowalne okno w Swift
Poniższy samodzielny program jest przeznaczony dla systemu macOS 14 lub nowszego i korzysta z aktualnego łańcucha narzędzi Swift oraz SDK macOS. Tytuł przykładowego okna to Local Editor, a tekst początkowy to Sample note. Tekst pozostaje w pamięci; ten przykład nie obsługuje zapisywania.
Zapisz kompletny program jako EditorDemo.swift. Użyj go jako samodzielnego pliku wykonywalnego, zamiast dodawać drugi punkt wejścia do istniejącej aplikacji. Swift dopuszcza tylko jeden punkt wejścia; poniższy typ @main go dostarcza. 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()
}
}
}
Na Macu z zainstalowanym Xcode lub Command Line Tools skompiluj i uruchom program w katalogu zawierającym plik:
xcrun swiftc -parse-as-library \
-target "$(uname -m)-apple-macos14.0" \
-framework AppKit EditorDemo.swift -o EditorDemo
./EditorDemo
Przykład sprawdzono na podstawie pobranej dokumentacji, ale nie udało się go skompilować ani uruchomić w tym środowisku badawczym, które działa pod kontrolą Linuksa i nie ma kompilatora Swift. Sprawdź działanie okna w macOS, wykonując poniższe czynności.
Dlaczego ta konfiguracja rozwiązuje problem z obsługą klawiatury
Metoda pomocnicza scrollableTextView() zwraca NSScrollView; kod pobiera z niego widok tekstu za pomocą documentView. Umieszczenie edytora tekstu w widoku przewijanym zapewnia temu małemu przykładowi prosty kontener dla treści wielowierszowej. Fabryka jest dostępna od macOS 10.14. Apple: scrollableTextView()
Kod dołącza ten kontener przed zażądaniem fokusu. Ustawia też initialFirstResponder, które Apple definiuje jako widok wybierany przy pierwszym wyświetleniu okna. Późniejsze wywołanie makeFirstResponder(_:) jawnie wskazuje edytor i zwraca wynik do sprawdzenia. Początkowy fokus i późniejsze żądanie fokusu dotyczą różnych etapów cyklu życia okna. Apple: initialFirstResponder, Apple: makeFirstResponder(_:)
Okno korzysta ze stylu z paskiem tytułu, dzięki czemu może stać się aktywne. makeKeyAndOrderFront(nil) żąda zarówno wyświetlenia okna, jak i nadania mu statusu aktywnego. Apple odróżnia tę metodę od orderFront(_:), która zmienia kolejność wyświetlania bez zmiany aktywnego okna. Apple: canBecomeKey, Apple: makeKeyAndOrderFront(_:)
Wywołanie activate() sprawia, że ten konkretny program wymaga systemu macOS 14 lub nowszego. Apple opisuje aktywację jako żądanie, a nie gwarancję. Jeśli okno się pojawi, ale nie będzie przyjmować wpisywanego tekstu, aktywuj aplikację, klikając jej okno, i sprawdź ponownie. Nie traktuj natychmiastowego komunikatu diagnostycznego jako obietnicy, że aktywacja już się zakończyła. Apple: activate()
Punkt wejścia i delegat są oznaczone @MainActor, którego executor Apple definiuje jako równoważny głównej kolejce dyspozytorskiej. Program uruchamia też pętlę zdarzeń AppKit za pomocą run(), dzięki czemu AppKit może obsługiwać zdarzenia użytkownika po zakończeniu konfiguracji. Apple: MainActor, Apple: run()
Zdiagnozuj widoczny edytor, który nadal nie przyjmuje wpisywanego tekstu
Wykonaj kolejno poniższe czynności. Pozwalają one osobno sprawdzić dołączenie widoku, uprawnienia okna, wybór respondera i możliwość edycji, dzięki czemu każdy wynik wskazuje konkretne następne działanie.
| Sprawdzenie | Co oznacza wynik | Następne działanie |
|---|---|---|
textView.window === window |
Edytor należy do zamierzonego okna. | Jeśli wynik jest fałszywy, dokończ dołączanie hierarchii widoków przed zażądaniem fokusu. |
window.canBecomeKey |
Okno może uzyskać status aktywnego. | Jeśli wynik jest fałszywy, sprawdź jego styl lub niestandardową podklasę. |
window.isKeyWindow |
To okno jest aktualnie aktywne w aplikacji. | Aktywuj aplikację i wybierz zamierzone okno, a następnie sprawdź ponownie. |
textView.acceptsFirstResponder |
Edytor przyjmuje żądanie fokusu. | Jeśli wynik jest fałszywy, sprawdź konfigurację i nadpisane metody podklasy. |
window.firstResponder === textView |
Ten konkretny edytor jest pierwszym responderem. | Jeśli wynik jest fałszywy, sprawdź zmianę fokusu i bieżącego respondera. |
textView.isEditable |
Edycja przez użytkownika jest włączona. | Jeśli edycja ma być możliwa, ustaw wartość na true. |
Sprawdzenie respondera jest szczególnie przydatne, ponieważ makeFirstResponder(_:) ma subtelne, udokumentowane zachowanie: może zwrócić true, gdy żądany responder odmawia przyjęcia statusu pierwszego respondera, a okno samo zostaje pierwszym responderem. Apple zaleca też sprawdzenie acceptsFirstResponder przed jawnym wysłaniem żądania. Dlatego sprawdź zarówno wartość zwracaną, jak i tożsamość wynikowego firstResponder. Apple: makeFirstResponder(_:)
Wynik false może oznaczać, że istniejący pierwszy responder odmówił rezygnacji z tej roli. Sprawdź zachowanie tego respondera oraz wszelkie delegaty zaangażowane w edycję. Apple dokumentuje na przykład, że delegat widoku tekstu może odrzucić zakończenie edycji za pomocą textShouldEndEditing(_:). Powtarzanie tego samego żądania fokusu bez rozwiązania problemu odmowy nie usuwa jej przyczyny. Apple: makeFirstResponder(_:), Apple: NSTextView
Aby wykonać podstawowe sprawdzenie ręczne, wpisz Blue square, naciśnij Return, wpisz Green circle, a następnie użyj klawiszy strzałek i Delete, by edytować tekst. Zmień rozmiar okna i kontynuuj pisanie. Są to proponowane czynności weryfikacyjne, a nie zgłoszone wyniki testów. Po zakończeniu zamknij okno; próbka zostanie wtedy zakończona.
Co się zmienia w przypadku okna bez obramowania?
Widoczne okno bez obramowania może nadal nie móc stać się aktywne. Apple dokumentuje, że zwykły NSWindow bez stylu z paskiem tytułu domyślnie nie staje się aktywny. Obsługiwanym sposobem dostosowania jest nadpisanie canBecomeKey w podklasie NSWindow i zwrócenie true, jeśli dane okno ma odbierać dane z klawiatury. Apple: canBecomeKey
Zacznij od przykładu z paskiem tytułu, aby uzyskać działające wprowadzanie tekstu. Jeśli interfejs będzie później wymagać okna bez obramowania, zmień implementację okna, zachowując te same kontrole dołączenia, możliwości edycji i respondera. Zmiana wyglądu okna nie zastępuje tych wymagań.
Jak powinien działać ten sam edytor osadzony w SwiftUI?
Użyj NSViewRepresentable, aby zintegrować widok tekstu AppKit z hierarchią SwiftUI. Apple przypisuje tworzenie i początkową konfigurację do makeNSView(context:), aktualizowanie bieżącego stanu do updateNSView(_:context:), a koordynację komunikatów delegata lub target-action do koordynatora. Apple: NSViewRepresentable
W przypadku tego edytora praktycznym rozwiązaniem jest zwrócenie widoku przewijanego jako reprezentowanego widoku i przechowywanie odwołania do widoku tekstu dokumentu w koordynatorze lub niestandardowym kontenerze. Skonfiguruj możliwość edycji podczas tworzenia. Przekazuj zmiany tekstu wprowadzone przez użytkownika za pośrednictwem delegata, jeśli otaczający interfejs SwiftUI potrzebuje wiązania.
Traktuj dołączenie do okna jako osobne zdarzenie, niezależne od tworzenia. Przed zażądaniem fokusu sprawdź, czy window edytora ma wartość różną od nil. Podklasa AppKit może użyć viewDidMoveToWindow() do obserwowania zmian w hierarchii; Apple dokumentuje, że wartość nil w window oznacza, że widok nie znajduje się obecnie w żadnym oknie. To udokumentowany punkt cyklu życia, który można wykorzystać do zaimplementowania żądania fokusu uwzględniającego dołączenie. Apple: viewDidMoveToWindow()
Zgodnie z przyjętą polityką implementacji zażądaj początkowego fokusu raz dla zamierzonego wyświetlenia albo w odpowiedzi na jawną akcję, taką jak Focus Editor. Unikaj bezwarunkowego wywoływania makeFirstResponder(_:) przy każdej aktualizacji SwiftUI: taka polityka wielokrotnie przenosiłaby fokus z powrotem po wybraniu przez użytkownika innego elementu sterującego. W chwili wysłania żądania sprawdź okno zawierające widok, zamiast zakładać, że wcześniej zapamiętane okno nadal jest właściwe.
Unikaj też resetowania ciągu znaków edytora przy każdej aktualizacji. W opakowaniu opartym na wiązaniu porównuj stan wejściowy z bieżącym tekstem i przed zastąpieniem zawartości uwzględniaj tekst oznaczony oraz zaznaczenie. Samodzielny przykład inicjuje ciąg tylko raz; zsynchronizowany edytor wymaga jawnej polityki aktualizacji.
Pozwól SwiftUI określać rozmiar głównego reprezentowanego widoku. Apple ostrzega, że bezpośrednia zmiana frame lub bounds tego widoku koliduje z zarządzaniem układem przez SwiftUI. Bezpośrednie ustawianie ramki w samodzielnym programie AppKit nie jest więc wzorcem określania rozmiaru, który należy przenosić na główny widok zarządzany przez SwiftUI. Apple: NSViewRepresentable
Czy przyjmowanie wpisywanego tekstu wymaga niestandardowego stosu TextKit?
W tym przypadku użyj istniejącego mechanizmu edycji NSTextView. Apple opisuje go jako interfejs systemu tekstowego, który rysuje tekst oraz obsługuje zaznaczanie, modyfikowanie, zarządzanie wprowadzaniem i powiązania klawiszy. Zwykłe wpisywanie nie wymaga ponownego tworzenia tych funkcji. Apple: NSTextView
Wybór mechanizmu TextKit to osobna decyzja od ustawienia fokusu klawiatury. Apple dokumentuje, że init(frame:) domyślnie używa NSLayoutManager, a ręcznie złożony widok tekstu może korzystać z NSTextLayoutManager w macOS 12 lub nowszym za pośrednictwem wskazanego inicjalizatora. Wybierz mechanizm odpowiedni do wymagań układu; zmiana mechanizmu nie zastępuje ustawienia edytora jako pierwszego respondera. Apple: init(frame:textContainer:)
Jeden szczegół diagnostyczny wymaga uwagi: odczyt layoutManager niekoniecznie jest bierną inspekcją. Apple dokumentuje, że dostęp do tej właściwości w widoku tekstu lub kontenerze tekstu korzystającym z nowszego systemu może uruchomić tryb zgodności z NSLayoutManager. Przykład unika odczytywania tej właściwości i nie zakłada, z którego mechanizmu korzysta fabryka widoku przewijanego. Apple: NSTextView
Działający rezultat jest konkretny: zamierzone okno aplikacji jest aktywne, jego edytowalny widok tekstu jest pierwszym responderem, a wpisywanie zmienia wyświetlany tekst. Najpierw uzyskaj takie działanie w małym oknie, a następnie zachowaj te same kontrole, dodając integrację ze SwiftUI, niestandardowy układ lub kolejne elementy sterujące.
Źródła
- NSTextView — Apple.
- isEditable — Apple.
- keyWindow — Apple.
- makeFirstResponder(_:) — Apple.
- initialFirstResponder — Apple.
- makeKeyAndOrderFront(_:) — Apple.
- canBecomeKey — Apple.
- scrollableTextView() — Apple.
- activate() — Apple.
- MainActor — Apple.
- run() — Apple.
- NSViewRepresentable — Apple.
- viewDidMoveToWindow() — Apple.
- init(frame:textContainer:) — Apple.
- Attributes — The Swift Project.