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
onAppearandonDisappearfire relative to a view's lifecycle - How to use
onAppearto trigger a delayed data load withDispatchQueue.main.asyncAfter - How
onAppearon items inside aLazyVStackfires lazily as each item scrolls into view - The difference between attaching
onAppearto 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 LazyVStack — onDisappear 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 init — init 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
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
@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.@State var count: Int = 0— Counts how manyLazyVStackitems have scrolled into view. The navigation title displays this counter, giving a live readout as you scroll. This demonstrates thatonAppearon each item fires independently and progressively as the user scrolls.LazyVStack— UnlikeVStack,LazyVStackonly renders views that are near the visible area. This is critical for performance with 50+ items. The trade-off is thatonAppearfires at scroll time rather than at view creation time..onAppear { count += 1 }on the RoundedRectangle — This closure fires each time a specific row enters the viewport. After scrolling through all 50 items,countwill be 50. Note: if the user scrolls back up past a row that has already appeared,onAppearfires again when it re-enters the viewport..onAppear(perform:)on theScrollView— This fires once, when the entireScrollViewcontainer appears on screen (i.e., when the user navigates to this view). It schedules a state update 5 seconds in the future usingDispatchQueue.main.asyncAfter. Thedeadline: .now() + 5expression means "5 seconds from the moment this line executes.".onDisappear(perform:)— Fires when theScrollViewleaves the screen. Here it resetsmyTextto a closing message. In a real app you might cancel aTimer, invalidate a subscription, or save scroll position here..navigationTitle("On Appear: \(count)")— Readscountdirectly in the title string. Becausecountis@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 onAppearinit 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
onAppearis 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,onAppearfires per item as it scrolls into view, enabling efficient lazy loading, analytics, and pagination patterns. onDisappearis 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