W aplikacji na Maca z edytowalnymi wierszami jednoliniowymi obsłuż klawisz Return za pomocą onSubmit, wstaw nowy model bezpośrednio pod bieżącym wierszem i ustaw fokus na jego polu przy użyciu FocusState. Nadaj każdemu wierszowi stabilny identyfikator, aby wstawienie zachowało powiązanie między tekstem, wierszem i fokusem.

W tym samouczku zbudujemy niewielki, lokalny edytor listy z przykładowymi pozycjami, takimi jak Item A. Naciśnięcie klawisza Return tworzy pusty wiersz pod edytowanym polem, zachowuje istniejący tekst i przenosi fokus klawiatury na nowe pole. Tekst nie jest dzielony w miejscu wstawiania.

Który kontroler tekstowy obsługuje takie działanie?

Użyj jednoliniowego TextField, gdy każdy wiersz reprezentuje jeden krótki wpis. Apple wyraźnie dokumentuje, że TextField wywołuje akcję onSubmit, gdy użytkownik naciska sprzętowy lub programowy klawisz Return. Apple dokumentuje również, że przypisanie wartości FocusState przenosi fokus na powiązany widok. Oba interfejsy API są dostępne w macOS 12 i nowszych. Apple: onSubmit(of:_:), Apple: FocusState.

Wybierz kontroler odpowiednio do edytowanej treści:

Zadanie edycji Punkt wyjścia Działanie klawisza Return do zaimplementowania
Jeden krótki wpis w każdym wierszu Jednoliniowy TextField Zatwierdź, wstaw poniżej, ustaw fokus na nowym wierszu
Akapit w każdym wierszu TextEditor lub widok tekstowy AppKit Określ, czym wstawienie wiersza różni się od wstawienia znaku nowej linii
Dokument z niestandardowymi poleceniami tekstowymi NSTextView osadzony w SwiftUI Obsłuż odpowiednie polecenie tekstowe i skoordynuj zmiany modelu

Apple opisuje TextEditor jako wieloliniowy, przewijany edytor. Jego dokumentacja nie określa takiej samej gwarancji zatwierdzania po naciśnięciu Return, jaką opisano dla TextField. Dlatego przy zmianie przykładu na TextEditor trzeba ponownie przeanalizować obsługę poleceń; samo dodanie tego samego modyfikatora nie jest udokumentowanym odpowiednikiem. Apple: TextEditor, Apple: onSubmit(of:_:).

Oddziel tożsamość wiersza od jego pozycji

Trzy wartości pełnią różne funkcje: identyfikator wiersza wskazuje wpis, indeks tablicy określa miejsce wstawienia, a identyfikator fokusu wskazuje pole, które powinno otrzymywać dane z klawiatury.

ForEach firmy Apple buduje widoki na podstawie danych mających identyfikatory. Użyj modelu Identifiable z identyfikatorem zapisywanym podczas tworzenia wiersza. W tym rozwiązaniu ten sam identyfikator staje się również wartością fokusu wiersza. Dzięki temu wpis zachowuje swoją tożsamość, gdy powyżej zostanie wstawiony inny wpis. Apple: ForEach.

Nie używaj edytowanego tekstu jako identyfikatora: dwa puste wiersze miałyby tę samą tożsamość, a edycja zmieniałaby tę tożsamość. Pozycje w tablicy również nie są dobrymi identyfikatorami w tym zadaniu, ponieważ zmieniają się po wstawieniu. Są to konsekwencje proponowanego modelu danych, a nie dodatkowe wymagania SwiftUI.

Każde pole potrzebuje odrębnej wartości fokusu. Apple ostrzega, że powiązanie tej samej wartości fokusu z wieloma widokami tworzy niejednoznaczne powiązanie fokusu. Zapisane UUID dla każdego wiersza zapewnia prosty sposób rozróżnienia pól. Apple: FocusState.

Zbuduj kompletny przykład dla macOS

Utwórz w Xcode projekt aplikacji macOS używający SwiftUI i ustaw minimalną obsługiwaną wersję systemu na macOS 12 lub nowszą. Zastąp wygenerowany punkt wejścia aplikacji i widok zawartości poniższym pojedynczym plikiem, usuwając pierwotną deklarację aplikacji, aby cel zawierał tylko jeden punkt wejścia @main.

Przykład używa zwykłego VStack wewnątrz widoku przewijanego. Zapisuje nowy wiersz jako oczekujący cel fokusu, a następnie ustawia fokus, gdy to pole się pojawi. Jest to strategia implementacyjna wynikająca z interfejsów API Apple dotyczących fokusu i pojawiania się widoków; Apple nie dokumentuje wywołań zwrotnych pojawienia się jako uniwersalnej gwarancji gotowości do ustawienia fokusu. Dokładny moment wywołania onAppear zależy od widoku. Apple: onAppear(perform:).

import SwiftUI
import Foundation
import Dispatch

private struct EditableRow: Identifiable {
    let id = UUID()
    var text: String
}

@main
struct RowEntryDemoApp: App {
    var body: some Scene {
        WindowGroup {
            RowEditor()
        }
    }
}

private struct RowEditor: View {
    @State private var rows = [
        EditableRow(text: "Item A"),
        EditableRow(text: "Item B")
    ]
    @State private var pendingFocusID: UUID?
    @FocusState private var focusedRowID: UUID?

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Local row editor")
                .font(.headline)

            Text("Press Return in a field to insert a row below it.")
                .font(.subheadline)

            ScrollViewReader { proxy in
                ScrollView {
                    VStack(spacing: 10) {
                        ForEach($rows) { $row in
                            TextField("New item", text: $row.text)
                                .textFieldStyle(.roundedBorder)
                                .focused($focusedRowID, equals: row.id)
                                .onSubmit {
                                    insertRow(after: row.id)
                                }
                                .id(row.id)
                                .onAppear {
                                    focusIfPending(row.id) {
                                        proxy.scrollTo(row.id, anchor: .center)
                                    }
                                }
                        }
                    }
                    .padding(4)
                }
            }
        }
        .padding(20)
        .frame(minWidth: 420, minHeight: 280)
    }

    private func insertRow(after id: UUID) {
        guard pendingFocusID == nil,
              let index = rows.firstIndex(where: { $0.id == id })
        else { return }

        let newRow = EditableRow(text: "")
        pendingFocusID = newRow.id
        rows.insert(newRow, at: index + 1)
    }

    private func focusIfPending(
        _ id: UUID,
        scroll: @escaping () -> Void
    ) {
        guard pendingFocusID == id else { return }

        DispatchQueue.main.async {
            guard pendingFocusID == id,
                  rows.contains(where: { $0.id == id })
            else { return }

            scroll()
            focusedRowID = id
            pendingFocusID = nil
        }
    }
}

Model zawiera wszystkie przykładowe dane, a plik definiuje każdy używany typ i każdą funkcję. W środowisku badawczym nie było dostępny kompilator Swift ani środowisko wykonawcze macOS, dlatego program został sprawdzony, ale nie został tu skompilowany ani uruchomiony.

Jak współdziałają wstawianie i ustawianie fokusu

Powiązanie ciągu znaków pola aktualizuje odpowiedni wiersz podczas edycji. Dzięki temu obsługa zatwierdzenia może wstawić nowy wiersz bez zastępowania ani czyszczenia bieżącego wpisu. Apple dokumentuje ciągłe aktualizacje pól tekstowych powiązanych z ciągiem znaków. Apple: TextField.

Procedura obsługi wstawiania znajduje zatwierdzany wiersz na podstawie identyfikatora i wstawia element pod indeksem index + 1. W przypadku środkowego wiersza nowy wpis trafia przed następny istniejący wpis. W przypadku ostatniego wiersza pozycja jest równa endIndex tablicy, co jest prawidłową pozycją wstawienia i powoduje dopisanie elementu. Apple: insert(_:at:).

Wywołanie zwrotne pojawienia się nowego pola sprawdza, czy jego identyfikator odpowiada oczekującemu żądaniu. Blok wysłany do kolejki ponownie to sprawdza przed przewinięciem widoku i ustawieniem fokusu. Dzięki tym kontrolom niezwiązane wywołanie zwrotne pojawienia się nie przejmie żądania. Przykład ignoruje też kolejne zatwierdzenia, gdy oczekuje żądanie fokusu.

Dodatkowe wysłanie bloku odracza pracę poza wywołanie zwrotne pojawienia się; nie jest udokumentowaną barierą renderowania. Ten wybór harmonogramowania należy zweryfikować w obsługiwanych wersjach macOS i rzeczywistej hierarchii kontenerów.

Przewijanie i ustawianie fokusu to osobne operacje. scrollTo odnajduje element potomny za pomocą identyfikatora widoku i przewija w jego kierunku; przypisanie focusedRowID żąda ustawienia fokusu klawiatury. Jawne .id(row.id) wskazuje cel przewijania. Apple: scrollTo(_:anchor:).

Sprawdź wynik i diagnozuj typowe problemy

Wykonaj poniższą sekwencję sprawdzającą na przykładowych wpisach:

  1. Kliknij Item A, wpisz dodatkowy tekst i naciśnij raz klawisz Return.
  2. Sprawdź, czy między dwoma pierwotnymi wpisami pojawiło się jedno puste pole.
  3. Od razu wpisz Item C i sprawdź, czy pojawiło się w nowym polu.
  4. Naciśnij Return w ostatnim polu i sprawdź, czy nowy wiersz pojawił się na końcu.
  5. Naciśnij Return w pustym polu i sprawdź, czy można dodać kolejny pusty wiersz.
  6. Kontynuuj, aż trzeba będzie przewinąć widok, a następnie sprawdź, czy wstawione pole staje się widoczne i przyjmuje tekst.

Są to oczekiwane wyniki, które należy sprawdzić lokalnie. Przykład celowo dopuszcza puste wpisy i zachowuje cały bieżący ciąg znaków, nawet jeśli punkt wstawiania znajduje się w jego środku. Jeśli zadanie wymaga podzielenia tego ciągu na dwa wpisy, konieczna będzie również obsługa zaznaczenia i zakresu tekstu.

Jeśli wiersz się pojawi, ale pisanie nadal trafia do starego pola, sprawdź identyfikator fokusu, wywołanie zwrotne pojawienia się nowego pola oraz to, czy okno jest aktywne. Sprawdź też, czy inne wywołanie zwrotne nie resetuje fokusu. Nie generuj ponownie identyfikatorów wierszy podczas obliczania widoku.

Jeśli wstawienie następuje dwukrotnie, sprawdź procedury obsługi zatwierdzania w wyższych widokach hierarchii. Apple dopuszcza użycie onSubmit na poszczególnych kontrolkach i widokach zawierających, a submitScope opisuje jako sposób zatrzymania propagacji zatwierdzenia do wyższych procedur obsługi. Tylko jedna procedura obsługi powinna odpowiadać za wstawianie. Apple: onSubmit(of:_:).

Zmiana etykiety zatwierdzania nie implementuje nawigacji ani wstawiania. Apple definiuje submitLabel jako ustawienie semantycznej etykiety zatwierdzania. Zmiana modelu i przypisanie fokusu nadal wymagają osobnego kodu. Apple: submitLabel(_:).

W przypadku znacznie większej kolekcji ponownie rozważ użycie stosu tworzącego wszystkie widoki od razu. Apple zauważa, że kontenery takie jak List i LazyVStack pobierają elementy ForEach leniwie. Może więc być konieczne przewinięcie niewidocznego pola do widoku, zanim jego wywołanie zwrotne pojawienia się będzie mogło zakończyć żądanie fokusu. Nie zmieniaj kontenera bez przetestowania tej sekwencji. Apple: ForEach.

Kiedy używać AppKit i TextKit?

Użyj przykładu SwiftUI w przypadku krótkich, niezależnych wpisów. Gdy wiersze zawierają tekst wieloliniowy lub wymagają precyzyjnego zachowania poleceń tekstowych, jedną z obsługiwanych możliwości jest osadzenie własnego NSTextView za pomocą NSViewRepresentable. Apple opisuje typy representable jako most umożliwiający tworzenie i aktualizowanie widoków AppKit w SwiftUI, przy czym koordynator przekazuje interakcje delegatowi. Apple: NSViewRepresentable.

W tym rozwiązaniu zachowaj ten sam model identyfikatorów wierszy i zaimplementuj następującą sekwencję:

  1. Utwórz widok tekstowy i przypisz koordynator jako jego delegata.
  2. Przekazuj zmiany tekstu do modelu odpowiedniego wiersza.
  3. Zaimplementuj textView(_:doCommandBy:) i rozpoznaj polecenie insertNewline(_:) zgodnie z wybranym działaniem edytora.
  4. Jeśli obsługujesz to polecenie jako wstawienie wiersza, zachowaj bieżący tekst, wstaw nowy wiersz i zwróć true, aby przejąć obsługę polecenia. Zwróć false w przypadku poleceń, których delegat nie obsługuje.
  5. Gdy nowy widok tekstowy zostanie dołączony do okna nadrzędnego, zażądaj fokusu za pomocą metody makeFirstResponder(_:) tego okna.

Apple dokumentuje zarówno polecenie nowej linii, jak i kontrakt wartości logicznej delegata: true oznacza, że delegat obsłużył polecenie, a false pozwala widokowi tekstowemu spróbować je wykonać. Zapewnia to jawną ścieżkę obsługi poleceń w edytorze należącym do aplikacji. Apple: insertNewline(_:), Apple: textView(_:doCommandBy:).

Przy dodawaniu niestandardowej obsługi poleceń zachowaj aktywne wprowadzanie tekstu. AppKit udostępnia hasMarkedText(), które informuje o tekście oznaczonym; wykorzystaj ten stan przy podejmowaniu decyzji, czy delegat powinien pozostawić polecenie systemowi tekstowemu. Przetestuj w gotowym edytorze wprowadzanie tekstu w trakcie kompozycji, zamiast zakładać, że każda interakcja związana z klawiszem Return powinna wstawiać wiersz. Apple: hasMarkedText().

W kwestii fokusu Apple wyraźnie zaleca użycie makeFirstResponder(_:) okna nadrzędnego zamiast bezpośredniego wywoływania becomeFirstResponder() widoku tekstowego. Dostęp do układu TextKit ma też konsekwencję zależną od wersji: w macOS 12 i nowszych jawny dostęp do layoutManager widoku tekstowego przełącza go w tryb zgodności korzystający z NSLayoutManager. Samo wstawianie wiersza nie wymaga takiego dostępu. Apple: NSTextView.

W przypadku zadania jednoliniowego zacznij od kompletnego przykładu SwiftUI i sprawdź w swojej aplikacji na Maca sekwencję wstawiania, wyświetlania i wpisywania tekstu. Stabilna tożsamość, jawna pozycja wstawienia i precyzyjne żądanie fokusu zapewniają podstawowe działanie; przejdź na widok tekstowy AppKit, gdy zadanie edycji wymaga jego obsługi poleceń i zaznaczenia.

Źródła