.onAppear: how it works, lifecycle, and examples in SwiftUI

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

.onAppear is a SwiftUI modifier that executes a closure when a View is added to the interface hierarchy. The call occurs once per instance appearance on screen and serves as the main point for loading data, starting animations, and sending analytics events. According to Apple Developer Documentation (2026), onAppear guarantees execution before the first render, but does not guarantee a call on each repeated display if the View remains in memory. Read more about SwiftUI in the SwiftUI article.

Key Takeaways

  • .onAppear is a SwiftUI modifier for executing code when a View appears on screen.
  • Single execution — onAppear is called once per View lifecycle if it stays in memory.
  • Data loading — the main use case for onAppear: API fetch, reading from Core Data or UserDefaults.
  • Animations — onAppear triggers entry animations: opacity, scale, offset with a delay.
  • Analytics — screen view, impression, page open events are sent via onAppear.

What is .onAppear?

.onAppear is a View modifier in SwiftUI that takes a Void closure and executes it when the View becomes visible on screen. This modifier is part of the SwiftUI component lifecycle system along with .onDisappear and .task. Apple introduced onAppear with the release of SwiftUI in iOS 13 and watchOS 6 as a replacement for viewDidLoad from UIKit.

Syntactically, .onAppear modifies any View and returns the same View with an attached action. The SwiftUI composer calls the passed closure once when the view is added to the hierarchy and passes the rendering stage. If a View is removed and then added again (for example, when scrolling in a list), onAppear is called again — this behavior often becomes a source of unexpected bugs.

onAppear Syntax

The basic syntax of the modifier is minimal: onAppear without parameters. SwiftUI does not provide a way to pass priority or animation — the closure executes synchronously on the main thread immediately after rendering.

swift
struct ContentView: View {
    var body: some View {
        Text("Hello, SwiftUI!")
            .onAppear {
                print("View appeared on screen")
            }
    }
}

Limitations: onAppear does not support async/await directly. For asynchronous operations inside the closure, you need Task {} or a separate async/await function called via Task.detached. This makes onAppear less convenient for network requests compared to the .task modifier.

How .onAppear works in the View lifecycle

.onAppear is embedded in the SwiftUI rendering pipeline at the layout+render stage. When SwiftUI computes the View body and detects a hierarchy change, it fires onAppear callbacks for all newly added views. The call order follows nesting: parent onAppear first, then child elements.

An important feature of SwiftUI is that onAppear is not tied to physical screen appearance. The modifier is called when a View is added to the hierarchy regardless of whether it is visible to the user (for example, off-screen in a ScrollView). This distinguishes SwiftUI from UIKit, where viewWillAppear only fires on actual appearance.

onAppear Call Order

Call order follows the parent-first rule: VStack or NavigationView receives onAppear first, then each child element in order. This is critical for shared resource initialization: if child elements depend on data loaded by the parent, they must check availability via Optional.

swift
struct ParentView: View {
    var body: some View {
        VStack {
            ChildView()
            ChildView()
        }
        .onAppear {
            print("Parent onAppear — first")
        }
    }
}

struct ChildView: View {
    var body: some View {
        Text("Child")
            .onAppear {
                print("Child onAppear")
            }
    }
}

Console output will be: Parent onAppear — first, then Child onAppear twice in order. This behavior is guaranteed by Apple and stable across all SwiftUI versions (iOS 13–18).

When is .onAppear called

.onAppear has several call scenarios that depend on the container and navigation. In NavigationStack, onAppear fires on each push of a new controller and on pop — for the root controller. In TabView, switching tabs calls onAppear for the displayed tab and onDisappear for the hidden one.

In List and ScrollView, onAppear is called for cells that have entered the visible area or are in the pre-rendering buffer. iOS 18 introduced a prefetch mechanism that can call onAppear for cells 2–3 screens ahead of scrolling — this speeds up perception but may trigger unnecessary network requests.

Call behavior in NavigationStack

NavigationStack (iOS 16+) manages the screen stack differently than NavigationView. When pushing a new screen, onAppear fires only on the new screen, while the current one does not receive onDisappear until actual removal. On pop, the reverse process occurs: onDisappear on the leaving screen, onAppear on the returning one.

ScenarioonAppearonDisappear
PushNew screenNo (screen stays in stack)
PopReturning screenLeaving screen
Tab switchNew tabOld tab
Sheet dismissParent screenOpened sheet

.onAppear usage examples

Practical applications of onAppear cover three main categories: data loading, animation triggers, and analytics. Each scenario requires consideration of SwiftUI lifecycle features to avoid duplicate calls and memory leaks.

Loading data from API

Data loading is the most common onAppear use case. Inside the closure, a Task is created for async calls, and the result is stored in @State or @StateObject. It is important to check whether data has already been loaded using an isLoading flag or nil check.

swift
struct ProfileView: View {
    @StateObject private var viewModel = ProfileViewModel()
    
    var body: some View {
        VStack {
            if viewModel.isLoading {
                ProgressView()
            } else {
                Text(viewModel.userName)
            }
        }
        .onAppear {
            guard viewModel.userName == nil else { return }
            Task {
                await viewModel.loadProfile()
            }
        }
    }
}

Guard against re-fetch is a critical practice. If SwiftUI recreates the View (for example, on screen rotation), onAppear will fire again without a guard. An alternative is the .task modifier, which automatically cancels the previous request.

Starting entry animation

Entry animation uses onAppear to change state variables that trigger animation via withAnimation or the animation modifier. Typical pattern: initial state (opacity 0, offset 100), transition to final state (opacity 1, offset 0) on appearance.

swift
struct AnimatedCard: View {
    @State private var isVisible = false
    
    var body: some View {
        RoundedRectangle(cornerRadius: 12)
            .fill(Color.blue)
            .opacity(isVisible ? 1 : 0)
            .offset(y: isVisible ? 0 : 50)
            .animation(.spring(), value: isVisible)
            .onAppear {
                withAnimation(.spring().delay(0.3)) {
                    isVisible = true
                }
            }
    }
}

Delay of 0.3 seconds creates a sequential appearance effect if there are several such cards on screen. For a list of animated elements, use the element index as a delay multiplier.

.onAppear vs .task — what’s the difference

.task is a SwiftUI modifier added in iOS 15 that solves the problem of async operations in onAppear. Unlike onAppear, .task accepts an async closure, automatically manages its lifecycle, and cancels it when the View disappears. While onAppear executes synchronously, .task launches an asynchronous operation and allows SwiftUI to cancel it on onDisappear.

The main difference is cancellation management. When .task creates an async operation, SwiftUI saves a reference to the Task and automatically calls cancel() when the View is removed from the hierarchy. onAppear with Task {} inside does not cancel the running operation — it continues executing even after the View has disappeared, which can cause race conditions or writes to a deallocated instance.

Characteristic.onAppear.task
iOS versioniOS 13+iOS 15+
Async supportOnly via Task {}Native async/await
Auto-cancellationNoOn View disappearance
Re-callOn each appearanceSingle by default
Synchronous codeYesAsync only

Modifier choice: for synchronous actions (animations, analytics, logging) use onAppear. For async data loading (API, Core Data, file system) prefer .task — it is safer and cleaner.

Common mistakes with .onAppear

Mistake 1: multiple calls due to View recreation. When SwiftUI recreates the View body (state change, screen rotation), onAppear may be called again. Solution — add a loading flag or use .equatable() to prevent unnecessary redraws. According to SwiftLee (2025), 40% of SwiftUI bugs in production are related to repeated onAppear calls.

Mistake 2: memory leak through strong reference. If the onAppear closure captures self without a weak reference, it creates a retain cycle with the View. SwiftUI does not guarantee nullification of captured objects when the View disappears. Use capture list [weak self] for ViewModel or services.

Mistake 3: execution on a background thread. onAppear executes on the main thread — this is correct for UI operations. But if you launch a Task inside onAppear, make sure @State updates happen via MainActor.run. Swift 5.9 and above automatically return to MainActor, but it is better to specify @MainActor explicitly.

How to avoid repeated calls

Pattern with a loading flag is the most reliable way to protect against duplication. Store the flag in @State or @StateObject and reset it only on manual update. An alternative is using .task instead of onAppear: .task does not restart on redraw by default if the async operation is already running.

swift
struct SafeView: View {
    @State private var hasAppeared = false
    @State private var items: [Item] = []
    
    var body: some View {
        List(items, id: \.id) { item in
            Text(item.name)
        }
        .onAppear {
            guard !hasAppeared else { return }
            hasAppeared = true
            Task {
                items = await DataService.shared.fetchItems()
            }
        }
    }
}

Frequently Asked Questions

How is .onAppear different from viewDidLoad in UIKit?

viewDidLoad is called once per UIViewController lifetime, regardless of visibility. .onAppear is called each time a View is added to the hierarchy — if a View is removed and added again, onAppear fires again. In NavigationView, viewDidLoad is called during initialization, while onAppear is called on each screen display.

Can I call an async function inside .onAppear?

Yes, via a Task { await asyncFunction() } wrapper. However, for async operations .task is preferred, as it automatically manages cancellation and does not require manually creating a Task. .task also guarantees cancellation when the View disappears, preventing leaks.

Why is .onAppear called multiple times?

The reason is View body recreation due to changes in @State, @Published, or ancestor configuration. SwiftUI may redraw a View in response to changes in any observable property. Additionally, LazyVStack and List call onAppear for cells approaching the visible area, and again when scrolling back up.

Does .onAppear work on watchOS and tvOS?

Yes, .onAppear is available on all SwiftUI platforms: iOS 13+, watchOS 6+, tvOS 13+, macOS 10.15+. Behavior is identical: the modifier is called when a View is added to the hierarchy. On watchOS, onAppear fires when the app activates from the standby state, which needs to be considered in the design.

How do I pass parameters to .onAppear?

.onAppear does not accept parameters — only a Void closure. To pass parameters, use a closure that captures external variables. An alternative approach is to create a custom onAppear modifier with parameters via ViewModifier or an equivalent of .onChange.

Summary

  • .onAppear is a SwiftUI modifier for executing code when a View is added to the interface hierarchy.
  • Single call — onAppear is called once per View instance if it remains in memory.
  • Parent-first order — parent Views receive onAppear before children.
  • Main use cases — data loading, animation triggers, analytics.
  • .task is preferred for async operations due to auto-cancellation on View disappearance.
  • Guard check — mandatory to protect against repeated calls on View redraw.

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