Skip to content

How to use onAppear and onDisappear in SwiftUI | SwiftUI Bootcamp #46

onAppear and onDisappear are how SwiftUI views react to entering and leaving the screen — the equivalent of viewDidLoad/viewWillDisappear in UIKit. After this lesson you'll know when these modifiers fire, how to use them to trigger data loads and teardown logic, and how LazyVStack triggers onAppear per item as items scroll into view.

What You'll Learn

  • When onAppear and onDisappear fire relative to a view's lifecycle
  • How to use onAppear to trigger a delayed data load with DispatchQueue.main.asyncAfter
  • How onAppear on items inside a LazyVStack fires lazily as each item scrolls into view
  • The difference between attaching onAppear to a container vs. individual list items

Mental Model

Think of onAppear like the motion sensor on a shop entrance. When a customer (view) walks through the door (appears on screen), the sensor triggers — the lights come on, the music starts playing, the welcome screen loads. That's your onAppear closure firing: fetch data, start animations, begin timers.

onDisappear is the same sensor detecting the customer leaving. When the view exits the visible area — because the user navigated away, dismissed a sheet, or scrolled it out of a LazyVStackonDisappear fires. This is where you clean up: cancel timers, pause audio, mark a message as "read."

The key insight is that in a LazyVStack, every individual row has its own pair of motion sensors. As the user scrolls down, each row's onAppear fires just before the row becomes visible. As rows scroll off the top, their onDisappear fires. This is how infinite scroll and lazy image loading patterns work.

Detailed Explanation

onAppear fires after the view has been inserted into the view hierarchy and is about to become visible. It is the correct place for initial data loads, starting animations, and beginning timers. It is not the same as a view's initinit runs whenever the struct is created (which can happen many times), while onAppear fires only when the view actually appears on screen.

onDisappear fires when the view is removed from the visible hierarchy. In a NavigationView, this fires when you navigate forward (the source screen disappears behind the destination). In a TabView, it fires on the tab you leave when you switch to another tab. In a LazyVStack, it fires as items scroll off screen.

LazyVStack is a performance optimization: it only creates views that are close to the visible area. This makes onAppear inside a LazyVStack perfect for tracking analytics events ("user scrolled to item 37"), lazy loading images, or pagination ("when item 45 of 50 appears, fetch the next page").

Using DispatchQueue.main.asyncAfter(deadline: .now() + 5) inside onAppear simulates an async data load with a 5-second delay. In a real app you would replace this with a URLSession call, a Combine publisher, or an async/await task. The pattern — show a loading state, populate data, clear loading state — is identical regardless of the underlying mechanism.

Code Structure

OnAppearBootcamp.swift demonstrates two uses of onAppear. The outer ScrollView uses onAppear to schedule a delayed text update (simulating a network response) and onDisappear to reset the text when the user navigates away. The inner LazyVStack items each have their own onAppear that increments a counter, making the navigation title a live count of how many items have entered the viewport.

Complete Code

OnAppearBootcamp.swift

swift
import SwiftUI

struct OnAppearBootcamp: View {
    
    @State var myText: String = "Start text." // will be updated 5 seconds after the view appears
    @State var count: Int = 0                  // increments each time a lazy-loaded row enters the viewport
    
    var body: some View {
        NavigationView {
            ScrollView {
                Text(myText) // displays the current text state, updates automatically when myText changes
                LazyVStack {
                    ForEach(0..<50) { _ in
                        RoundedRectangle(cornerRadius: 25.0)
                            .frame(height: 200)
                            .padding()
                            .onAppear {
                                count += 1 // fires each time this individual row scrolls into view
                            }
                    }
                }
            }
            .onAppear(perform: {
                DispatchQueue.main.asyncAfter(deadline: .now() + 5) { // simulates a 5-second network delay
                    myText = "This is the new text!" // state update after the simulated load completes
                }
            })
            .onDisappear(perform: {
                myText = "Ending text." // reset state when the view leaves the screen (e.g., user navigates away)
            })
            .navigationTitle("On Appear: \(count)") // live count of how many items have appeared in the viewport
        }
    }
}

struct OnAppearBootcamp_Previews: PreviewProvider {
    static var previews: some View {
        OnAppearBootcamp()
    }
}

Code Walkthrough

  1. @State var myText: String = "Start text." — The text displayed at the top of the scroll view. It starts with a placeholder value and gets updated after the simulated async delay. This pattern mirrors the "loading state → populated state" flow of a real data fetch.

  2. @State var count: Int = 0 — Counts how many LazyVStack items have scrolled into view. The navigation title displays this counter, giving a live readout as you scroll. This demonstrates that onAppear on each item fires independently and progressively as the user scrolls.

  3. LazyVStack — Unlike VStack, LazyVStack only renders views that are near the visible area. This is critical for performance with 50+ items. The trade-off is that onAppear fires at scroll time rather than at view creation time.

  4. .onAppear { count += 1 } on the RoundedRectangle — This closure fires each time a specific row enters the viewport. After scrolling through all 50 items, count will be 50. Note: if the user scrolls back up past a row that has already appeared, onAppear fires again when it re-enters the viewport.

  5. .onAppear(perform:) on the ScrollView — This fires once, when the entire ScrollView container appears on screen (i.e., when the user navigates to this view). It schedules a state update 5 seconds in the future using DispatchQueue.main.asyncAfter. The deadline: .now() + 5 expression means "5 seconds from the moment this line executes."

  6. .onDisappear(perform:) — Fires when the ScrollView leaves the screen. Here it resets myText to a closing message. In a real app you might cancel a Timer, invalidate a subscription, or save scroll position here.

  7. .navigationTitle("On Appear: \(count)") — Reads count directly in the title string. Because count is @State, every increment automatically triggers a re-render that updates the navigation bar title without any extra code.

Common Mistakes

Mistake: Placing an expensive operation directly in a view's init instead of onAppear
init runs every time SwiftUI recreates the struct, which can be very frequent. Network calls, disk reads, or heavy computations placed in init cause unnecessary work and can degrade performance. onAppear fires only when the view becomes visible — it is the correct hook for initial data loading.

Mistake: Expecting onAppear to fire only once in a LazyVStack
In a LazyVStack, onAppear fires each time an item scrolls into the viewport — including when it scrolls back into view after being scrolled off screen. If your onAppear handler is not idempotent (safe to run multiple times), add a guard or a boolean flag to prevent duplicate work.

Mistake: Forgetting that onDisappear fires when navigating forward, not just when going back
In a NavigationView, pushing a new view onto the stack causes the source view to disappear — onDisappear fires. When the user pops back, the source view reappears — onAppear fires again. Design your onAppear/onDisappear handlers to handle this symmetric lifecycle correctly.

Key Takeaways

  • onAppear is the correct place for initial data loads and animation triggers — it fires when the view becomes visible, not when the struct is initialized.
  • In a LazyVStack, onAppear fires per item as it scrolls into view, enabling efficient lazy loading, analytics, and pagination patterns.
  • onDisappear is the cleanup hook — use it to cancel timers, invalidate subscriptions, or save ephemeral state when a view leaves the screen.

Last updated: June 27, 2026

Released under the MIT License.