Aby arkusz SwiftUI pozostał otwarty podczas aktualizacji, zachowaj tożsamość jego hosta prezentacji i utrzymuj aktywne powiązanie prezentacji. Dołącz arkusz do stabilnego kontenera, aby zmieniająca się zawartość podrzędna mogła się aktualizować bez zastępowania widoku prezentującego.
Ten poradnik jest przeznaczony dla programisty tworzącego aplikację na Maca z edytowalnym arkuszem, który powinien pozostać widoczny, gdy zmienia się interfejs znajdujący się pod nim. Przykład wykorzystuje syntetyczny tekst lokalny i rozdziela trzy elementy, które można sprawdzić niezależnie: stan prezentacji, czas życia widoku prezentującego oraz zawartość edytora.
Dlaczego aktualizacja widoku może spowodować zniknięcie arkusza?
SwiftUI może tworzyć nowe wartości widoków podczas obliczania body, zachowując jednocześnie tożsamość wyświetlanego widoku. Stan utrzymuje się przez czas życia tej tożsamości. Usunięcie widoku lub zmiana jego tożsamości kończy ten okres; przełączanie między gałęziami warunkowymi również może tworzyć odrębne tożsamości, nawet jeśli obie gałęzie zawierają ten sam typ widoku. Apple wyjaśnia te zależności w Demystify SwiftUI.
W przypadku arkusza sterowanego wartością Boolean udokumentowanym sposobem sterowania prezentacją jest powiązanie przekazywane do sheet(isPresented:onDismiss:content:): wartość true żąda prezentacji. Badając znikający arkusz, sprawdź zarówno to powiązanie, jak i to, czy widok z modyfikatorem nadal istnieje. Wartość true przechowywana gdzie indziej nie gwarantuje, że pierwotny host prezentacji przetrwał. Zobacz dokumentację Apple dotyczącą arkusza sterowanego wartością Boolean.
Szczególnie bezpośrednią przyczyną jest zmieniający się jawny identyfikator. Apple dokumentuje, że zmiana wartości przekazanej do id(_:) resetuje tożsamość widoku, w tym jego stan. Sprawdź widok prezentujący i jego przodków pod kątem identyfikatorów wyprowadzanych z liczników wersji, edytowanego tekstu lub nowo generowanych wartości. Usuń takie identyfikatory, gdy ich działanie resetujące jest niezamierzone. Zobacz id(_:).
Praktyczny wniosek projektowy jest taki, aby umieścić modyfikator arkusza poza zmieniającą się częścią interfejsu. Jeśli widok ładowania, filtrowany wiersz lub alternatywny układ szczegółów może zniknąć, najbliższy odpowiedni, trwały przodek jest zwykle lepszym właścicielem prezentacji.
Zbuduj stabilnego właściciela arkusza w kompletnej aplikacji na Maca
Utwórz w Xcode aplikację SwiftUI dla macOS, ustaw cel wdrożenia na macOS 12 lub nowszy i zastąp wygenerowane deklaracje aplikacji oraz widoku zawartości poniższym pojedynczym plikiem. Zachowaj tylko jedną deklarację aplikacji @main. Minimalna wersja macOS 12 jest wymagana przez używaną w przykładzie akcję środowiskową dismiss; sam TextEditor jest dostępny od macOS 11. Zobacz dokumentację Apple dotyczącą dismiss i TextEditor.
Widok główny okna przechowuje flagę prezentacji, edytowalny tekst, licznik wersji oraz flagę układu. Modyfikator arkusza znajduje się na zewnętrznym VStack. Zmieniają się tylko widoki wewnątrz tego kontenera, gdy przełączają się gałęzie warunkowe. Apple zaleca deklarowanie stanu jako prywatnego w najwyższym widoku, który go potrzebuje, oraz przekazywanie powiązań dzieciom, które muszą go zmieniać. Zobacz State.
import SwiftUI
@main
struct StableSheetDemoApp: App {
var body: some Scene {
WindowGroup {
SheetOwnerView()
}
}
}
struct SheetOwnerView: View {
@State private var isEditorPresented = false
@State private var note = "Sample note Alpha"
@State private var revision = 0
@State private var compactLayout = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Local Notes")
.font(.title)
if compactLayout {
Text("Compact summary: revision \(revision)")
} else {
VStack(alignment: .leading, spacing: 8) {
Text("Expanded summary")
Text("Revision \(revision)")
Text(note)
}
}
Button("Edit Note") {
isEditorPresented = true
}
}
.padding(24)
.frame(minWidth: 440, minHeight: 260)
.sheet(isPresented: $isEditorPresented) {
NoteEditorSheet(
note: $note,
revision: $revision,
compactLayout: $compactLayout
)
}
}
}
struct NoteEditorSheet: View {
@Binding var note: String
@Binding var revision: Int
@Binding var compactLayout: Bool
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Edit Sample Note")
.font(.headline)
TextEditor(text: $note)
.frame(minHeight: 160)
Text("Owner revision: \(revision)")
Button("Update Owner") {
revision += 1
compactLayout.toggle()
}
HStack {
Spacer()
Button("Done") {
dismiss()
}
}
}
.padding(24)
.frame(width: 460, height: 340)
}
}
To kompletna aplikacja demonstracyjna, sprawdzona pod kątem zgodności z udokumentowanymi interfejsami API. Nie została tutaj skompilowana ani uruchomiona, ponieważ w tym środowisku nie ma łańcucha narzędzi Swift ani środowiska wykonawczego macOS.
Jak sprawdzić aktualizację bez zamykania arkusza
- Uruchom aplikację i kliknij
Edit Note. - Zastąp przykładowy tekst ciągiem
Sample note Beta. - Kliknij kilkakrotnie
Update Owner. Każde kliknięcie zwiększa stan właściciela i przełącza zawartość warunkową właściciela. - Sprawdź, czy edytor pozostaje widoczny, jego tekst nadal brzmi
Sample note Beta, a wyświetlana wersja rośnie. - Kliknij
Done. Sprawdź, czy okno wyświetla edytowany tekst w rozwiniętym układzie, gdy ten układ jest aktywny. - Otwórz edytor ponownie i sprawdź, czy edytowany tekst nadal jest dostępny.
Przycisk aktualizacji znajduje się w arkuszu, więc test nie wymaga klikania okna znajdującego się pod nim. AppKit opisuje arkusze jako modalne dla dokumentu: gdy arkusz jest widoczny, większość zdarzeń kierowanych do jego okna nadrzędnego jest niedozwolona. Aktualizowanie właściciela przez powiązanie pozwala w tym przykładzie bezpośrednio sprawdzić zmianę stanu. Zobacz beginSheet(_:completionHandler:).
Zdecyduj, co powinno oznaczać zamknięcie edytora
Ten przykład od razu edytuje tekst właściciela. Done zamyka edytor; nie wykonuje osobnej operacji zapisu. Ciąg pozostaje w stanie lokalnym okna, a przykład nie zapewnia trwałości między uruchomieniami aplikacji. To zachowanie wynika z projektu współdzielonego powiązania opisanego w State.
Jeśli zadanie wymaga wersji roboczej, którą można anulować, użyj osobnej wartości roboczej i skopiuj ją do dokumentu dopiero po jawnym potwierdzeniu. Zdefiniuj to zachowanie przed wdrożeniem zamykania: decyzja o zatwierdzeniu lub odrzuceniu wersji roboczej jest odrębna od decyzji, który widok jest właścicielem arkusza.
Zdiagnozuj zamknięcie za pomocą trzech niezależnych kontroli
Wykonaj poniższe czynności w swojej aplikacji. Pozwolą przełożyć ogólne zasady tożsamości na ukierunkowane badanie bez założenia, że każdy znikający arkusz ma tę samą przyczynę.
| Kontrola | Co sprawdzić | Praktyczna poprawka |
|---|---|---|
| Wartość prezentacji | Każdy zapis ustawiający wartość Boolean na false lub czyszczący opcjonalny element prezentacji |
Zapisuj zmiany zamykające tylko w zamierzonych akcjach zamknięcia |
| Tożsamość widoku prezentującego | Gałęzie warunkowe, usuwane wiersze, zmieniające się identyfikatory przodków | Przenieś zarówno stan prezentacji, jak i modyfikator do odpowiedniego, stabilnego przodka |
| Czas życia zawartości | Stan lokalny arkusza, zmieniające się identyfikatory zawartości, synchronizacja edytora | Zachowaj zamierzoną sesję edycji i aktualizuj istniejącą zawartość w przemyślany sposób |
Najpierw wyszukaj w projekcie przypisania do właściwości prezentacji oraz wywołania dismiss(). Funkcja zwrotna odświeżania może jawnie zamykać arkusz. Odczytuj dismiss wewnątrz własnego widoku arkusza, tak jak w przykładzie: Apple dokumentuje, że akcja dotyczy środowiska, z którego ją pobrano. Odczytanie jej w widoku głównym okna może zamiast tego odnosić się do tego okna. Zobacz dismiss.
Następnie sprawdź hierarchię nad modyfikatorem. Jeśli arkusz jest dołączony do widoku szczegółów usuwanego podczas odświeżania, przenieś go ponad granicę, przy której następuje zastąpienie. Przenieś też lokalnie przechowywany stan prezentacji. Pozostawienie flagi w dziecku, którego tożsamość dobiega końca, naraża jego przechowywanie na zastąpienie, zgodnie z wyjaśnieniem Apple dotyczącym czasu życia widoku.
Po trzecie, odróżnij zamknięcie prezentacji od sytuacji, w której edytor pozostaje widoczny, ale traci wersję roboczą. Zacznij od prostego przykładu z TextEditor, a następnie etapami dodawaj własną zawartość. To metoda diagnostyczna, która pomaga ustalić, czy problem wynika z własności prezentacji, czy z logiki aktualizacji zawartości.
Czy warto przejść na arkusz sterowany elementem?
Użyj sheet(item:onDismiss:content:), gdy źródłem prezentacji jest konkretnie wybrany obiekt. Apple dokumentuje opcjonalny element zgodny z Identifiable, przekazuje do zamknięcia zawartości element różny od nil i opisuje zastępowanie, gdy element się zmienia. Zmiana przeciążenia sama w sobie nie zachowuje widoku prezentującego, którego czas życia dobiega końca. Zobacz arkusze sterowane elementem.
Wybierz identyfikator, który reprezentuje obiekt przez cały zamierzony czas sesji edycji. Protokół Swift Identifiable wyraża stabilną tożsamość, a jego dokumentacja wyraźnie pozostawia wymagany czas i zakres typom, które go implementują. Nowy identyfikator przy każdym odczycie właściwości nie może reprezentować tego samego, trwającego obiektu. Zobacz Identifiable.
W przypadku prostego edytora, takiego jak w przykładzie, flaga prezentacji Boolean wraz z osobnymi powiązaniami tekstu ułatwia sprawdzenie zamierzonej relacji. W edytorze wybranego elementu przechowuj tożsamość zaznaczenia oddzielnie od często zmienianych pól. Traktuj wybór innego obiektu jako świadomą decyzję dotyczącą prezentacji i najpierw określ, co ma stać się z bieżącą wersją roboczą.
Czy wyłączenie interaktywnego zamykania utrzyma arkusz?
interactiveDismissDisabled(_:) steruje zamykaniem nieprogramowym. Apple wyraźnie stwierdza, że nie zapobiega ono zamknięciu spowodowanemu zmianą powiązania prezentacji ani wywołaniem dismiss. Zgodnie z udokumentowaną rolą nie rozwiązuje więc problemu zastąpienia tożsamości. Zobacz interactiveDismissDisabled(_:).
Zastosuj ten modyfikator wewnątrz hierarchii arkusza tylko wtedy, gdy interfejs wymaga takiego zachowania przy zamykaniu, i zapewnij jawną ścieżkę zakończenia. W tym zadaniu zacznij od zachowania hosta i jego stanu. Dodanie ograniczenia zamykania przed ustaleniem przyczyny może pozostawić pierwotny problem własności nierozwiązany.
Co się zmienia, gdy arkusz zawiera zawartość AppKit lub TextKit?
W przypadku NSTextView osadzonego przez NSViewRepresentable oddziel tworzenie od aktualizacji. Apple definiuje makeNSView(context:) jako etap tworzenia, a updateNSView(_:context:) jako miejsce stosowania kolejnych informacji ze SwiftUI. Koordynator przekazuje zmiany delegata lub akcji celu z powrotem do SwiftUI. Zobacz NSViewRepresentable i makeNSView(context:).
Praktyczna zasada wynikająca z tego cyklu życia mówi, by nie przebudowywać edytora tylko dlatego, że zmienił się niezwiązany z nim stan właściciela. Przed zastąpieniem zawartości ustal, czy tekst przychodzący z modelu rzeczywiście się różni i jak należy obsługiwać równoczesne edycje lokalne. Stabilny arkusz sam w sobie nie rozwiązuje problemu konkurujących zapisów do jego tekstu.
TextKit zapewnia przechowywanie, układ i renderowanie tekstu. AppKit opakowuje te możliwości w NSTextView i udostępnia nowoczesne oraz starsze mechanizmy układu. Te mechanizmy dotyczą zawartości edytora; własność prezentacji nadal wymaga własnego stabilnego czasu życia. Używaj TextKit, gdy wymaga tego zadanie edycji, a zachowanie aktualizacji tekstu badaj oddzielnie od zamykania arkusza. Zobacz omówienie TextKit firmy Apple.
Kiedy prezentacja AppKit zarządzana przez okno jest odpowiednia?
Jeśli istniejąca aplikacja zarządza już cyklem życia okien w AppKit, macOS 26 wprowadza NSWindow.beginSheet(content:completionHandler:) do bezpośredniego prezentowania zawartości SwiftUI w tym oknie. Apple dokumentuje obsługę akcji środowiskowej zamykania arkusza, rozmiarów, pasków narzędzi i sterowania zamykaniem interaktywnym. Użyj SDK macOS 26 i uwzględnij granicę dostępności macOS 26 przed wdrożeniem tej funkcji. Zobacz beginSheet(content:completionHandler:).
Zwrócony NSWindow.HostingSheetRepresentation może zmieniać prezentowany widok główny lub umożliwiać programowe zamykanie z AppKit. Stanowi udokumentowaną opcję, gdy własność na poziomie okna pasuje do architektury aplikacji. Nie jest potrzebny w przedstawionym wyżej przykładzie ze stabilnym kontenerem SwiftUI. Zobacz NSWindow.HostingSheetRepresentation.
W starszych wersjach macOS istniejąca metoda AppKit beginSheet(_:completionHandler:) przyjmuje arkusz NSWindow i dokumentuje kolejkowanie prezentacji. Nie zakładaj, że nowsze przeciążenie z zawartością SwiftUI jest dostępne w tych systemach. Zobacz beginSheet(_:completionHandler:).
Zachowaj stabilnego właściciela, utrzymuj aktywną wartość prezentacji i zachowaj zamierzone źródło prawdy dla edytora. Sprawdź te warunki za pomocą małej lokalnej akcji aktualizacji, zanim przywrócisz bardziej złożoną zawartość. Dzięki temu uzyskasz konkretny sposób na kontynuowanie edycji podczas aktualizacji interfejsu, w granicach czasu życia i interfejsów API opisanych przez Apple.
Źródła
- Demystify SwiftUI — Apple
- sheet(isPresented:onDismiss:content:) — Apple
- id(_:) — Apple
- State — Apple
- dismiss — Apple
- TextEditor — Apple
- sheet(item:onDismiss:content:) — Apple
- Identifiable — Apple
- interactiveDismissDisabled(_:) — Apple
- NSViewRepresentable — Apple
- makeNSView(context:) — Apple
- TextKit — Apple
- beginSheet(content:completionHandler:) — Apple
- NSWindow.HostingSheetRepresentation — Apple
- beginSheet(_:completionHandler:) — Apple