.onDisappear — modyfikator SwiftUI, który wykonuje domknięcie przy usuwaniu View z hierarchii interfejsu. Wywołanie następuje przy zamknięciu ekranu, przełączaniu zakładki, dismiss okna modalnego lub przewinięciu elementu poza widoczny obszar. Według Apple Developer Documentation (2026), onDisappear nie gwarantuje wywołania w scenariuszach awaryjnego zakończenia lub przy zniszczeniu aplikacji przez system-watchdog. Więcej o SwiftUI przeczytasz w materiale o SwiftUI.
Najważniejsze
.onDisappear — modyfikator View w SwiftUI, który przyjmuje domknięcie Void i wykonuje je przy usuwaniu View z hierarchii. W parze z .onAppear tworzy pełny cykl życia ekranu: pojawienie się — działanie — zniknięcie. Apple wprowadziła onDisappear wraz z wydaniem SwiftUI w iOS 13 jako odpowiednik viewDidDisappear z UIKit.
Składniowo .onDisappear jest identyczny z onAppear: modyfikuje dowolny View i dołącza domknięcie, które jest wywoływane przez komponent SwiftUI przy usuwaniu widoku. W przeciwieństwie do UIKit, gdzie viewDidDisappear jest wywoływany dopiero po zakończeniu animacji przejścia, onDisappear w SwiftUI może być wywołany przed zakończeniem animacji — w momencie, gdy View jest oznaczony do usunięcia.
Podstawowa składnia onDisappear jest równie zwięzła jak onAppear. Modyfikator nie przyjmuje dodatkowych parametrów — tylko domknięcie, które jest wykonywane synchronicznie w głównym wątku.
struct DetailView: View {
var body: some View {
Text("Ekran szczegółów")
.onDisappear {
print("DetailView zniknął z ekranu")
}
}
}
Mechanizm wywołania onDisappear jest przeciwny do onAppear: najpierw elementy potomne otrzymują onDisappear, potem rodzic. Ta zasada child-first gwarantuje, że zasoby elementów potomnych są zwalniane przed zwolnieniem zasobów rodzica. Jeśli element potomny zależy od danych rodzica, musi mieć możliwość poprawnego zakończenia bez kontekstu rodzica.
SwiftUI wywołuje onDisappear w momencie, gdy View jest wykluczany z grafu renderowania. Wyzwalaczem może być: pop z NavigationStack, przełączenie TabView, dismiss okna modalnego (sheet/fullScreenCover), zmiana warunkowo wyświetlanego View (if/switch). W List i ScrollView onDisappear jest wywoływany przy przewijaniu komórki poza prefetch-bufor.
Kolejność child-first oznacza, że jeśli w VStack są trzy potomne View, onDisappear zostanie wywołany najpierw dla każdego dziecka w odwrotnej kolejności, potem dla rodzica. Jest to krytyczne dla poprawnego czyszczenia: potomne timery są anulowane zanim rodzicielski ViewModel zwolni współdzielone zasoby.
struct ParentView: View {
var body: some View {
VStack {
ChildView(id: "A")
ChildView(id: "B")
}
.onDisappear {
print("Parent onDisappear — ostatni")
}
}
}
struct ChildView: View {
let id: String
var body: some View {
Text("Dziecko \(id)")
.onDisappear {
print("Dziecko \(id) onDisappear")
}
}
}
Wynik w konsoli: Child B onDisappear, Child A onDisappear, Parent onDisappear — ostatni. Odwrotna kolejność w porównaniu z onAppear gwarantuje poprawny łańcuch zwalniania.
Scenariusze wywołania onDisappear zależą od typu kontenera. W NavigationStack onDisappear jest wyzwalany przy pop-to-root, zwykłym pop-back lub usunięciu ekranu gestem przesunięcia (interactivePopGestureRecognizer). W TabView przełączenie zakładki wywołuje onDisappear dla ukrywanej i onAppear dla wyświetlanej — oba modyfikatory są wyzwalane praktycznie jednocześnie.
W Sheet i fullScreenCover onDisappear jest wywoływany przy programowym dismiss (przez @Environment(\.dismiss)) lub geście przesunięcia w dół. Ważna cecha: jeśli sheet został otwarty, ale użytkownik przełączył aplikację, onDisappear NIE jest wywoływany do faktycznego zamknięcia.
| Scenariusz | onDisappear wywoływany | Uwaga |
|---|---|---|
| Pop w NavigationStack | Tak | Natychmiast po animacji |
| Przełączenie zakładki TabView | Tak | Bieżąca zakładka |
| Dismiss sheet | Tak | Przed zakończeniem animacji |
| Przewinięcie w List | Tak | Komórka poza prefetch-strefą |
| Zwijanie aplikacji | Nie | Brak gwarancji wywołania |
| Crash/watchdog kill | Nie | Nie jest wywoływany |
Główne scenariusze onDisappear — to czyszczenie zasobów, zapisywanie stanu i śledzenie. W przeciwieństwie do onAppear, zadania onDisappear są wykonywane na wyjściu i nie wymagają sprawdzania duplikacji, ponieważ View znika jednorazowo.
Zapisywanie stanu w onDisappear jest szczególnie przydatne dla formularzy, gdzie dane muszą być zapisane przy opuszczeniu ekranu. Timery i subskrypcje Combine są anulowane w onDisappear, aby uniknąć wycieków przy powrocie na ekran.
struct FormView: View {
@State private var draftText = ""
@State private var timer: Timer?
var body: some View {
TextField("Enter text", text: $draftText)
.onAppear {
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
saveDraft()
}
}
.onDisappear {
timer?.invalidate()
timer = nil
saveDraft()
}
}
private func saveDraft() {
UserDefaults.standard.set(draftText, forKey: "draft")
}
}
Anulowanie timera w onDisappear zapobiega wykonaniu kodu po zamknięciu ekranu. Bez anulowania timer może próbować zaktualizować @State, który już nie należy do aktualnego View, wywołując runtime warning.
Czas przebywania na ekranie — klasyczny scenariusz śledzenia. Rejestrujemy czas w onAppear, obliczamy różnicę w onDisappear i wysyłamy zdarzenie analityczne z czasem trwania sesji.
struct TrackedView: View {
@State private var appearTime: Date?
var body: some View {
Text("Śledzony ekran")
.onAppear {
appearTime = Date()
Analytics.shared.logEvent("screen_view", params: ["screen": "TrackedView"])
}
.onDisappear {
if let start = appearTime {
let duration = Date().timeIntervalSince(start)
Analytics.shared.logEvent("screen_close", params: [
"screen": "TrackedView",
"duration_ms": Int(duration * 1000)
])
}
}
}
}
Kluczowa różnica onDisappear od onAppear — kolejność wywołania i gwarancje zadziałania. onAppear jest wywoływany przy dodawaniu View do hierarchii i ma właściwość ponownego wywołania przy odtworzeniu. onDisappear jest wywoływany przy usuwaniu i gwarantowanie działa tylko przy normalnym zamknięciu, ale nie w scenariuszach awaryjnych.
Według WWDC 2024, Apple zaleca traktować onDisappear jako punkt czyszczenia, a nie jako punkt zapisu danych. Krytycznie ważne dane (płatności, rejestracje) powinny być zapisywane w czasie rzeczywistym, a nie w momencie znikania View, ponieważ onDisappear nie gwarantuje wywołania przy zwijaniu aplikacji.
| Cecha | .onAppear | .onDisappear |
|---|---|---|
| Moment wywołania | Dodanie View do hierarchii | Usunięcie View z hierarchii |
| Kolejność | Parent-first | Child-first |
| Gwarancja | Wysoka | Brak przy awaryjnych zakończeniach |
| Główne zadanie | Inicjalizacja | Czyszczenie |
| Ponowne wywołanie | Przy odtworzeniu View | Jednokrotnie na zniknięcie |
Zalecenie: używaj onDisappear tylko do niekrytycznego czyszczenia i śledzenia. Do zapisywania danych używaj scenePhase lub powiadomień applicationWillTerminate w AppDelegate.
Scenariusz 1: ładowanie podobne do YouTube. Na ekranie szczegółów wideo onDisappear zapisuje pozycję odtwarzania w UserDefaults. Przy ponownym otwarciu onAppear przywraca pozycję z UserDefaults, dając efekt ciągłego oglądania.
Scenariusz 2: anulowanie subskrypcji Combine. Jeśli ViewModel używa publisherów Combine, onDisappear anuluje subskrypcję przez cancellable?.cancel(). Zapobiega to aktualizacji UI po opuszczeniu ekranu, co jest szczególnie ważne dla list z paginacją i zapytań wyszukiwania.
Scenariusz 3: zamykanie WebSocket. W aplikacjach z połączeniami czasu rzeczywistego (komunikatory, kanały notowań) onDisappear zamyka połączenie WebSocket w celu oszczędzania baterii i transferu. Ponowne otwarcie następuje w onAppear przy powrocie na ekran.
WebSocket — typowy zasób, który należy zamykać przy opuszczeniu ekranu. W poniższym przykładzie onDisappear rozłącza socket, a onAppear ponownie go podłącza, co daje znaczną oszczędność transferu dla aplikacji z wieloma ekranami.
struct ChatView: View {
@StateObject private var socket = WebSocketManager()
var body: some View {
ChatListView(messages: socket.messages)
.onAppear {
socket.connect()
}
.onDisappear {
socket.disconnect()
}
}
}
Ważne: przy przełączaniu między zakładkami TabView onDisappear bieżącej i onAppear następnej zakładki są wyzwalane prawie jednocześnie. Dla WebSocket może to prowadzić do cyklu disconnect-connect, który generuje dodatkowe obciążenie. Rozwiązaniem jest użycie opóźnienia lub sprawdzenia, czy socket jest potrzebny na następnym ekranie.
Często zadawane pytania
Tak, .onDisappear nie gwarantuje wywołania przy awaryjnym zakończeniu aplikacji (crash, watchdog kill), zwinięciu aplikacji bez zamknięcia ekranu lub scenariuszu systemowym, w którym aplikacja jest niszczona w tle. Dla krytycznie ważnych danych używaj scenePhase lub applicationWillTerminate.
.onDisappear jest wyzwalany przy usuwaniu View z hierarchii — lokalny callback dla konkretnego ekranu. .scenePhase (przez @Environment(\.scenePhase)) jest wyzwalany przy zmianie stanu całej aplikacji: active, inactive, background. Do śledzenia czasu na ekranie używaj onAppear+onDisappear, do zapisywania globalnego stanu — scenePhase.
Przyczyną jest szybkie przełączanie między zakładkami lub wielokrotny push/pop tego samego ekranu. SwiftUI może utworzyć nową instancję View, usunąć starą, utworzyć ponownie — za każdym razem wywołując onDisappear i onAppear. Sprawdź, czy nie używasz .id(), .equatable() lub czy nie odtwarzasz View w ciele rodzica.
Tak, .onDisappear jest w pełni obsługiwany na macOS (10.15+) z tym samym zachowaniem: wywoływany przy zamknięciu okna, usunięciu panelu split view lub dismiss okna modalnego. Na macOS onDisappear jest również wywoływany przy ukryciu okna (hide), a nie tylko przy zamknięciu, co jest ważne dla aplikacji macOS.
Zapisz referencję do URLSessionTask w @State i wywołaj task.cancel() w onDisappear. Alternatywnie użyj modyfikatora .task, który automatycznie anuluje operację async przy zniknięciu View. .task jest preferowany dla wszystkich operacji async, w tym URLSession.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również