.onDisappear: how it works and use in SwiftUI

Author: IT Sectr Published: 2026-06-26 Reading time: 10 min

.onDisappear is a SwiftUI modifier that executes a closure when a View is removed from the interface hierarchy. The call occurs when a screen is closed, a tab is switched, a modal window is dismissed, or an element scrolls out of view. According to Apple Developer Documentation (2026), onDisappear does not guarantee a call in crash scenarios or when the app is terminated by the system watchdog. Learn more about SwiftUI in the SwiftUI article.

Key Takeaways

  • .onDisappear is a SwiftUI modifier for executing code when a View disappears.
  • Symmetry — onDisappear is paired with onAppear and is called in reverse order.
  • Resource cleanup — the main use case: canceling timers, closing connections, saving state.
  • Analytics — onDisappear is used to send screen close, dwell time, and session end events.
  • No guarantee — onDisappear is not called when the app crashes.

What is .onDisappear?

.onDisappear is a View modifier in SwiftUI that takes a Void closure and executes it when the View is removed from the hierarchy. Together with .onAppear, it forms the complete lifecycle of a screen: appearance — work — disappearance. Apple introduced onDisappear along with SwiftUI in iOS 13 as an analogue of viewDidDisappear from UIKit.

Syntactically, .onDisappear is identical to onAppear: it modifies any View and attaches a closure that is called by the SwiftUI renderer when the view is removed. Unlike UIKit, where viewDidDisappear fires only after the transition animation completes, onDisappear in SwiftUI can be called before the animation finishes — at the moment the View is marked for removal.

onDisappear syntax

Basic syntax of onDisappear is as concise as onAppear. The modifier takes no additional parameters — only the closure, which executes synchronously on the main thread.

swift
struct DetailView: View {
    var body: some View {
        Text("Detail Screen")
            .onDisappear {
                print("DetailView disappeared from screen")
            }
    }
}

How .onDisappear works

Call mechanism of onDisappear is the opposite of onAppear: child elements receive onDisappear first, then the parent. This child-first rule ensures that child resources are freed before the parent's resources are released. If a child element depends on parent data, it must be able to terminate correctly without the parent context.

SwiftUI calls onDisappear when the View is removed from the render graph. Triggers include: pop from NavigationStack, TabView switching, modal dismiss (sheet/fullScreenCover), or a conditionally rendered View (if/switch). In List and ScrollView, onDisappear is called when a cell scrolls beyond the prefetch buffer.

Child-first call order

Child-first order means that if a VStack has three child Views, onDisappear is called first for each child in reverse order, then for the parent. This is critical for proper cleanup: child timers are canceled before the parent ViewModel releases shared resources.

swift
struct ParentView: View {
    var body: some View {
        VStack {
            ChildView(id: "A")
            ChildView(id: "B")
        }
        .onDisappear {
            print("Parent onDisappear — last")
        }
    }
}

struct ChildView: View {
    let id: String
    var body: some View {
        Text("Child \(id)")
            .onDisappear {
                print("Child \(id) onDisappear")
            }
    }
}

Console output: Child B onDisappear, Child A onDisappear, Parent onDisappear — last. The reverse order compared to onAppear ensures a correct cleanup chain.

When .onDisappear is called

Call scenarios for onDisappear depend on the container type. In NavigationStack, onDisappear fires on pop-to-root, regular pop-back, or screen dismissal via swipe (interactivePopGestureRecognizer). In TabView, switching tabs calls onDisappear for the hidden tab and onAppear for the shown one — both modifiers fire almost simultaneously.

In Sheet and fullScreenCover, onDisappear is called on programmatic dismiss (via @Environment(\.dismiss)) or a downward swipe gesture. An important detail: if a sheet was opened but the user switched apps, onDisappear is NOT called until the actual close.

ScenarioonDisappear calledNote
Pop in NavigationStackYesImmediately after animation
TabView tab switchYesCurrent tab
Sheet dismissYesBefore animation completes
Scroll in ListYesCell left prefetch zone
App minimizationNoNo guarantee of call
Crash/watchdog killNoNot called

.onDisappear examples

Main use cases for onDisappear are resource cleanup, state saving, and tracking. Unlike onAppear, onDisappear tasks execute on exit and do not need deduplication checks since the View disappears once.

State saving and timer cancellation

State saving in onDisappear is especially useful for forms where data must be saved when leaving the screen. Timers and Combine subscriptions are canceled in onDisappear to prevent leaks when returning to the screen.

swift
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")
    }
}

Timer cancellation in onDisappear prevents code execution after the screen is already closed. Without cancellation, the timer might try to update @State that no longer belongs to the current View, causing a runtime warning.

Screen time tracking

Dwell time on a screen is a classic tracking scenario. Record the time in onAppear, calculate the difference in onDisappear, and send an analytics event with the session duration.

swift
struct TrackedView: View {
    @State private var appearTime: Date?
    
    var body: some View {
        Text("Tracked Screen")
            .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)
                    ])
                }
            }
    }
}

.onDisappear vs .onAppear — differences

The key difference between onDisappear and onAppear is the call order and execution guarantees. onAppear is called when a View is added to the hierarchy and may be called again on recreation. onDisappear is called on removal and is guaranteed to fire only on normal close, not in crash scenarios.

According to WWDC 2024, Apple recommends treating onDisappear as a cleanup point, not a data commit point. Critically important data (payments, registrations) should be saved in real time, not at the moment the View disappears, since onDisappear is not guaranteed to be called when the app is minimized.

Characteristic.onAppear.onDisappear
Call momentView added to hierarchyView removed from hierarchy
OrderParent-firstChild-first
GuaranteeHighNot on crashes
Main taskInitializationCleanup
Repeated callOn View recreationOnce per disappearance

Recommendation: use onDisappear only for non-critical cleanup and tracking. For data persistence, use scenePhase or applicationWillTerminate notifications in AppDelegate.

Typical use cases

Scenario 1: YouTube-like playback. On the video detail screen, onDisappear saves the playback position to UserDefaults. On reopening, onAppear restores the position from UserDefaults, creating a continuous viewing experience.

Scenario 2: Cancel Combine subscription. If a ViewModel uses Combine publishers, onDisappear cancels the subscription via cancellable?.cancel(). This prevents UI updates after leaving the screen, which is especially important for lists with pagination and search queries.

Scenario 3: Close WebSocket. In apps with real-time connections (messengers, ticker feeds), onDisappear closes the WebSocket connection to save battery and data. Reconnection happens in onAppear when returning to the screen.

Closing WebSocket in onDisappear

WebSocket is a typical resource that needs to be closed when leaving the screen. In the example below, onDisappear disconnects the socket, and onAppear reconnects it, providing significant data savings for apps with multiple screens.

swift
struct ChatView: View {
    @StateObject private var socket = WebSocketManager()
    
    var body: some View {
        ChatListView(messages: socket.messages)
            .onAppear {
                socket.connect()
            }
            .onDisappear {
                socket.disconnect()
            }
    }
}

Important: when switching between TabView tabs, onDisappear of the current tab and onAppear of the next tab fire almost simultaneously. For WebSocket this can cause a disconnect-connect cycle that creates unnecessary load. The solution is to use a delay or check whether the socket is needed on the next screen.

Frequently Asked Questions

Can .onDisappear fail to be called?

Yes, .onDisappear does not guarantee a call on app crash (crash, watchdog kill), app minimization without closing the screen, or system scenarios where the app is terminated in the background. For critical data, use scenePhase or applicationWillTerminate.

How to distinguish .onDisappear from .scenePhase?

.onDisappear fires when a View is removed from the hierarchy — a local callback for a specific screen. .scenePhase (via @Environment(\.scenePhase)) fires when the entire app state changes: active, inactive, background. Use onAppear+onDisappear for screen time tracking and scenePhase for global state persistence.

Why does .onDisappear get called multiple times in a row?

The cause is rapid tab switching or repeated push/pop of the same screen. SwiftUI may create a new View instance, remove the old one, create again — each time calling onDisappear and onAppear. Check whether you are using .id(), .equatable(), or recreating the View in the parent body.

Does .onDisappear work in SwiftUI on macOS?

Yes, .onDisappear is fully supported on macOS (10.15+) with the same behavior: called on window close, split view panel removal, or modal dismiss. On macOS, onDisappear is also called on window hide (not just close), which is important to consider for macOS apps.

How to cancel URLSessionTask in .onDisappear?

Store a reference to the URLSessionTask in @State and call task.cancel() in onDisappear. Alternatively, use the .task modifier, which automatically cancels the async operation when the View disappears. .task is preferred for all async operations, including URLSession.

Summary

  • .onDisappear — paired modifier to onAppear for resource cleanup and tracking when a View disappears.
  • Child-first order — child Views receive onDisappear before the parent for a correct cleanup chain.
  • No call guarantee — on crash, minimization, or system termination, onDisappear may not fire.
  • Main use cases — canceling timers, closing WebSocket, saving state, tracking dwell time.
  • Analytics — onDisappear sends screen_close events with dwell time calculated via Date().timeIntervalSince.
  • For async operations — use .task instead of onDisappear with Task {}, since .task automatically cancels requests.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also