What is the difference between .task() and .onAppear()?

Both run when a view appears, but .task is async and cancelled with the view’s lifetime, while .onAppear is synchronous and does not cancel work for you

Answer

The core idea

Both .onAppear and .task run work when a SwiftUI view appears. The difference is how that work is scheduled and how long it is allowed to live.

.onAppear runs synchronous setup. .task runs asynchronous work whose lifetime matches the view, and SwiftUI cancels that task when the view disappears or changes identity.

Neither modifier is guaranteed to run only once.

1. .onAppear is for synchronous appearance side effects

.onAppear(perform:) takes a normal closure. Use it for quick, synchronous work tied to appearance: analytics, focusing a field, resetting local UI flags.

swift 5.2
struct ProfileScreen: View {
    @FocusState private var isSearchFocused: Bool

    var body: some View {
        TextField("Search", text: .constant(""))
            .focused($isSearchFocused)
            .onAppear {
                isSearchFocused = true // ✅ sync UI setup
            }
    }
}

It does not give you an async context. Any network call or await inside requires wrapping in something else — usually an unstructured Task.

2. .task is async and tied to the view’s lifetime

.task adds an asynchronous task that starts before the view appears. Inside the closure you can await directly. Per Apple’s docs, if the work has not finished when SwiftUI removes the view or the view’s identity changes, SwiftUI cancels that task.

swift 5.2
struct CatalogScreen: View {
    @State private var items: [Item] = []

    var body: some View {
        List(items) { item in
            Text(item.title)
        }
        .task {
            items = await loadItems() // ✅ async work tied to this view
        }
    }
}

That cancellation is cooperative: long-running code should check Task.isCancelled or call Task.checkCancellation() at safe points, and APIs underneath should propagate cancellation when they can.

.task(id:) restarts the work when a dependency changes — for example reloading when a selected userID updates — and cancels the previous run.

3. How many times they run

There is no fixed call count. Both fire when the view appears, and that can happen more than once for the same screen:

  • switch away from a tab and back
  • push a detail and pop back to the list
  • scroll a row out of a List / ScrollView and back in
  • structural identity changes that tear the view down and build it again

So treat them as appearance events, not “run once per screen forever.” A plain .task { await load() } will load again after those reappearances. If the product rule is truly once per node lifetime, gate with @State (for example hasLoaded), or make the restart intentional with .task(id:).

Body redraws alone do not mean another call. Changing unrelated @State re-runs body, but .onAppear / .task only re-fire when appearance or the task’s identity rules say so.

Why it matters

Screens that push, pop, or scroll away often still have in-flight loads. Prefer .task for async appearance work so SwiftUI cancels with the view. Because both modifiers can also re-fire on return, assuming “once” leads to duplicate network calls every time the user comes back.

Interview angle

Lead with the one-line distinction: sync appearance hook versus async, view-lifetime task with automatic cancellation. Add that both can run multiple times on reappearance (tabs, navigation, list recycling), and that cancellation is cooperative. Primary references: task(priority:_:) and onAppear(perform:).