Przypisz delegata NSTextFieldDelegate, zaimplementuj controlTextDidEndEditing(_:) i skopiuj stringValue pola do powiązania SwiftUI. Koordynator łączy to wywołanie zwrotne ze stanem widoku, dzięki czemu etykieta może się zaktualizować po zakończeniu edycji.
W tym samouczku zbudujemy małą aplikację na Maca z dwiema edytowalnymi etykietami i podsumowaniem pokazującym zatwierdzone wartości. Celem jest pozostawienie podsumowania bez zmian podczas pisania, a następnie zaktualizowanie go po zakończeniu sesji edycji przez AppKit. Apple dokumentuje, że wywołanie zwrotne kończące edycję następuje po zatwierdzeniu zmian przez kontrolkę. Dokumentacja Apple dotycząca controlTextDidEndEditing(_:)
Które zdarzenie powinno aktualizować widok?
W przypadku istniejącego NSTextField użyj wywołania zwrotnego delegata obsługującego edycję kontrolki. Delegat otrzymuje powiadomienie, którego object jest kontrolką, która je wysłała, dzięki czemu procedura obsługi może zidentyfikować pole i odczytać jego wartość. Dokumentacja Apple dotycząca textDidEndEditingNotification
Wybierz zdarzenie odpowiednio do zachowania interfejsu, którego potrzebujesz:
| Oczekiwane zachowanie | Odpowiednia ścieżka |
|---|---|
| Zaktualizowanie zatwierdzonej wartości po zakończeniu edycji pola tekstowego AppKit | Zaimplementuj w jego delegacie controlTextDidEndEditing(_:). |
| Wywołanie istniejącej obsługi target-action AppKit po zakończeniu edycji | Skonfiguruj właściwość komórki sendsActionOnEndEditing. |
Obsługa zatwierdzenia z natywnego SwiftUI TextField |
Dołącz .onSubmit dla udokumentowanego zachowania przy zatwierdzaniu. |
Te ścieżki mają różne kontrakty. Apple dokumentuje, że włączenie sendsActionOnEndEditing wysyła akcję po naciśnięciu Return, przejściu klawiszem Tab poza pole lub kliknięciu innego pola tekstowego; wyłączenie tej opcji ogranicza akcję do naciśnięcia Return. Dokumentacja SwiftUI określa, że .onSubmit dla TextField reaguje na naciśnięcie Return. Nie ustanawia więc ogólnego wywołania zwrotnego przy utracie fokusu dla osadzonego pola AppKit. Dokumentacja Apple dotycząca sendsActionOnEndEditing, dokumentacja Apple dotycząca onSubmit(of:_:)
W tej aplikacji delegat jest jedynym miejscem zatwierdzania tekstu pola. Jeśli później dodasz obsługę target-action, nie wykonuj tego samego efektu zatwierdzenia w obu procedurach obsługi.
Zbuduj kompletny przykład SwiftUI i AppKit
Utwórz w Xcode projekt aplikacji macOS, używając języka Swift i interfejsu SwiftUI. W tym przykładzie użyj niedawnej wersji łańcucha narzędzi obsługującej Swift 6 i ustaw docelową wersję macOS 13 lub nowszą. Zastąp wygenerowany plik wejściowy aplikacji kompletnym programem poniżej i usuń wygenerowany plik ContentView.swift, jeśli deklaruje on inną strukturę ContentView. Pozostaw dokładnie jeden punkt wejścia @main. Dokumentacja Apple dotycząca App opisuje tę strukturę punktu wejścia. Dokumentacja Apple dotycząca App
Docelowa wersja macOS 13 w tym przykładzie wynika z wyboru konfiguracji projektu, a nie z daty wprowadzenia mostka: Apple podaje, że NSViewRepresentable jest dostępne od macOS 10.15. Zastosowanie AppKit sprawia, że ta implementacja jest przeznaczona dla macOS. Dokumentacja Apple dotycząca NSViewRepresentable
import SwiftUI
import AppKit
@main
@MainActor
struct EditingDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@MainActor
struct ContentView: View {
@State private var cardTitle = "Blue Card"
@State private var shelfLabel = "Shelf A"
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Card title")
CommitTextField(value: $cardTitle, label: "Card title")
.frame(height: 24)
Text("Shelf label")
CommitTextField(value: $shelfLabel, label: "Shelf label")
.frame(height: 24)
Divider()
Text("Committed values")
.font(.headline)
Text("Card: \(cardTitle)")
Text("Shelf: \(shelfLabel)")
}
.padding(20)
.frame(minWidth: 420)
}
}
@MainActor
struct CommitTextField: NSViewRepresentable {
@Binding var value: String
let label: String
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeNSView(context: Context) -> NSTextField {
let field = NSTextField(string: value)
field.placeholderString = label
field.delegate = context.coordinator
return field
}
func updateNSView(_ field: NSTextField, context: Context) {
context.coordinator.parent = self
field.placeholderString = label
field.isEnabled = context.environment.isEnabled
// Leave the active editing buffer alone.
if field.currentEditor() == nil, field.stringValue != value {
field.stringValue = value
}
}
static func dismantleNSView(
_ field: NSTextField,
coordinator: Coordinator
) {
field.delegate = nil
}
@MainActor
final class Coordinator: NSObject, NSTextFieldDelegate {
var parent: CommitTextField
init(parent: CommitTextField) {
self.parent = parent
super.init()
}
func controlTextDidEndEditing(_ notification: Notification) {
guard let field = notification.object as? NSTextField else {
return
}
let committedValue = field.stringValue
if parent.value != committedValue {
parent.value = committedValue
}
}
}
}
To kompletny przykład aplikacji z syntetycznymi wartościami lokalnymi. Został sprawdzony pod kątem pobranej dokumentacji API, ale nie został tu skompilowany ani uruchomiony: dostępne środowisko wykonawcze to Linux, bez łańcucha narzędzi Swift i SDK macOS. Skompiluj i uruchom go w Xcode na Macu.
Jak wartość trafia do podsumowania
Każda wartość nadrzędna @State dostarcza powiązanie do opakowania pola. Koordynator zapisuje przez to powiązanie po zakończeniu edycji. SwiftUI aktualizuje wtedy podsumowanie, ponieważ odczytuje zmieniony stan. Apple dokumentuje zarówno aktualizację widoku zależną od stanu, jak i użycie powiązań, które pozwalają widokom podrzędnym modyfikować stan należący do widoku nadrzędnego. Dokumentacja Apple dotycząca State
makeNSView tworzy edytowalną kontrolkę AppKit i przypisuje koordynatora jako jej delegata. updateNSView obsługuje zmiany przepływające ze SwiftUI do tej kontrolki. Wywołanie zwrotne delegata obsługuje przepływ w przeciwnym kierunku. W wytycznych dotyczących integracji Apple wykorzystuje ten sam układ koordynatora i powiązania do osadzania widoków AppKit. Sesja Apple „Use SwiftUI with AppKit”
Przypisanie context.coordinator.parent = self zapewnia aktualność reprezentowanej przez koordynator wartość. Koordynator istnieje przez cały czas życia osadzonego widoku, natomiast wartości reprezentowalnego widoku mogą się zmieniać. Apple wyraźnie pokazuje odświeżanie tej przechowywanej wartości podczas updateNSView. Wyjaśnienie cyklu życia koordynatora przez Apple
Zachowaj wersję roboczą podczas aktualizacji SwiftUI
Najważniejszym warunkiem jest field.currentEditor() == nil. W tym przykładzie niedokończony tekst pozostaje w AppKit, a SwiftUI zachowuje ostatnią zatwierdzoną wartość. Bez warunku sprawdzającego edycję skopiowanie zatwierdzonej wartości do pola podczas aktualizacji SwiftUI mogłoby zastąpić wersję roboczą.
Apple dokumentuje, że currentEditor() zwraca edytor pola kontrolki albo nil, jeśli go nie ma. Edytor pola to obiekt NSTextView współdzielony przez kontrolki w oknie. Dlatego sprawdzanie wyłącznie, czy firstResponder okna jest równe NSTextField, to kiepski substytut sprawdzenia, jaki edytor ma kontrolka. Dokumentacja Apple dotycząca currentEditor()
Wynikające z tego zachowanie jest jawną polityką projektu:
| Sytuacja | Przykładowe zachowanie |
|---|---|
| Pole jest nieaktywne, a jego powiązanie ulega zmianie | Skopiuj wartość powiązania do pola przy następnej aktualizacji reprezentowalnego widoku. |
| Pole ma aktywny edytor | Zachowaj bufor edycji. |
| Edycja kończy się tekstem innym niż dotychczasowy | Zapisz ukończony tekst w powiązaniu. |
| Edycja kończy się niezmienionym tekstem | Pomiń zbędne przypisanie stanu. |
Ta polityka nadaje lokalnej wersji roboczej pierwszeństwo podczas sesji edycji. Jeśli inny kod aplikacji zmieni to samo powiązanie, gdy użytkownik edytuje tekst, późniejsze zatwierdzenie może nadpisać tę zmianę. W przykładzie nie ma innego kodu zapisującego tę wartość. W większym edytorze zdecyduj, jak rozstrzygać równoczesne zmiany — na przykład zakończ bieżącą sesję przed przełączeniem wybranego elementu albo przechowuj osobno wersję roboczą i zatwierdzoną, oferując jawny wybór. Nie odrzucaj niedokończonego tekstu wyłącznie po to, by zsynchronizować ponowne rysowanie.
Do tego zadania nie jest potrzebna personalizacja układu TextKit. Istotna granica przebiega na końcu sesji edycji kontrolki i jej edytora pola. Przewodnik Apple po architekturze tekstu wyjaśnia relację między NSTextField, jego delegatem i współdzielonym edytorem pola NSTextView; ten przewodnik jest zarchiwizowany, natomiast aktualna dokumentacja currentEditor() potwierdza model współdzielonego edytora. Przewodnik Apple „Text Editing”, aktualna dokumentacja Apple dotycząca edytora pola
Sprawdź zachowanie po zakończeniu edycji w aplikacji
Po skompilowaniu aplikacji wykonaj poniższe czynności, aby sprawdzić przykład. Są to oczekiwane wyniki wynikające z implementacji i udokumentowanego zachowania podczas edycji, a nie zgłoszone rezultaty uruchomienia.
- Zastąp
Blue CardtekstemGreen Card. Podczas pisania podsumowanie powinno nadal pokazywaćCard: Blue Card. - Naciśnij Return. Podsumowanie powinno zmienić się na
Card: Green Card. - Ponownie edytuj tytuł karty, a następnie kliknij pole
Shelf label. Ukończony tytuł karty powinien pojawić się w podsumowaniu. - Edytuj wartość półki, a następnie naciśnij Tab. Sprawdź, czy podsumowanie półki zostało zaktualizowane, i przyjrzyj się nawigacji klawiaturą.
- Zatwierdź pustą wartość. Odpowiednie podsumowanie powinno wyświetlać prefiks bez wartości; przykład dopuszcza puste ciągi.
- Zakończ edycję bez zmieniania tekstu. Sprawdzenie równości powinno zapobiec kolejnemu przypisaniu stanu.
Naciśnięcie Return może zakończyć edycję bez oczekiwanego przeniesienia fokusu. Apple dokumentuje, że po powiadomieniu o zakończeniu edycji NSTextField próbuje wykonać swoją akcję i obsłużyć inne zdarzenia Return, a także może zaznaczyć tekst. Klawisze Tab i Shift-Tab próbują przenieść fokus odpowiednio do następnego lub poprzedniego widoku kluczowego. Podczas sprawdzania interfejsu traktuj zakończenie edycji i miejsce docelowe fokusu jako osobne zachowania. Dokumentacja Apple dotycząca textDidEndEditing(_:)
Diagnozowanie braku wywołania zwrotnego
Najpierw sprawdź, czy field.delegate nadal wskazuje koordynatora. Ta właściwość jest słaba, więc tymczasowy obiekt delegata bez osobnego właściciela nie pozostanie dostępny. Koordynator reprezentowalnego widoku zapewnia tu wymagany czas życia obiektu. Dokumentacja Apple dotycząca delegate, wyjaśnienie cyklu życia koordynatora przez Apple
Następnie sprawdź, czy edycja rzeczywiście się zakończyła. Kliknięcie dowolnego tła nie jest wiarygodnym testem tego warunku. W tym przykładzie użyj klawisza Return albo innego edytowalnego pola. Jeśli wynik jest inny, sprawdź konkretne przejście między obiektami odpowiedzialnymi za obsługę zdarzeń.
Jeśli aplikacja dodaje walidację, sprawdź control(_:textShouldEndEditing:). Zgodnie z dokumentacją zwracana wartość określa, czy edycja może się zakończyć. Odrzuconej próby opuszczenia pola nie należy traktować jako pomyślnego zatwierdzenia. Dokumentacja Apple dotycząca control(_:textShouldEndEditing:)
Sprawdź też ścieżki anulowania. Apple wyraźnie stwierdza, że wywołanie abortEditing() w celu odrzucenia oczekujących zmian nie wywołuje controlTextDidEndEditing(_:). Operacja anulowania wymaga więc własnego, przemyślanego zachowania stanu. Dokumentacja Apple dotycząca wywołania zwrotnego kończącego edycję
Żądanie zakończenia edycji z jawnego polecenia aplikacji
Jeśli w przyszłości polecenie Done będzie miało odwołanie do tego pola, obsługiwaną ścieżką przez mechanizm responderów jest wywołanie na jego własnym oknie metody makeFirstResponder(nil). Przed kontynuowaniem sprawdź zwróconą wartość logiczną: Apple dokumentuje, że bieżący responder może odmówić rezygnacji z obsługi, powodując zwrócenie przez metodę wartości false. Użyj rzeczywistego okna pola, zamiast zakładać, że kluczowe okno aplikacji należy do tego edytora. Dokumentacja Apple dotycząca makeFirstResponder(_:)
Dostęp AppKit i wywołania zwrotne interfejsu w tym mostku powinny działać na głównym aktorze. Obecna deklaracja delegata jest izolowana przez główny aktor, a dokumentacja Swifta określa, że MainActor używa wykonawcy równoważnego głównej kolejce wysyłania. Przykład wyraża tę izolację bezpośrednio za pomocą @MainActor; między zakończeniem edycji a zapisem powiązania nie dodaje odroczonego zadania. Deklaracja wywołania zwrotnego Apple, dokumentacja Swifta dotycząca MainActor
Implementacja ma jedną wyraźną granicę zatwierdzania: AppKit kończy edycję, koordynator odczytuje pole, a powiązanie aktualizuje zależny tekst SwiftUI. Ochrona wersji roboczej podczas aktualizacji reprezentowalnego widoku sprawia, że ten schemat dobrze sprawdza się przy codziennej edycji etykiet, bez konieczności stosowania własnych mechanizmów układu tekstu.
Źródła
- controlTextDidEndEditing(_:) — dokumentacja Apple dla deweloperów
- textDidEndEditingNotification — dokumentacja Apple dla deweloperów
- sendsActionOnEndEditing — dokumentacja Apple dla deweloperów
- onSubmit(of:_:) — dokumentacja Apple dla deweloperów
- App — dokumentacja Apple dla deweloperów
- NSViewRepresentable — dokumentacja Apple dla deweloperów
- State — dokumentacja Apple dla deweloperów
- Use SwiftUI with AppKit — Apple Developer
- currentEditor() — Apple Developer
- Text Editing — archiwum dokumentacji Apple
- textDidEndEditing(_:) — dokumentacja Apple dla deweloperów
- delegate — dokumentacja Apple dla deweloperów
- control(_:textShouldEndEditing:) — dokumentacja Apple dla deweloperów
- makeFirstResponder(_:) — dokumentacja Apple dla deweloperów
- MainActor — dokumentacja Apple dla deweloperów