When do onAppear, task and onChange run, and which one do you use for async work?

8.4.1 · L2 · Interview

Predict first

A view uses .task { await load() } and the user navigates away before it finishes. What happens to the task?

// A: it keeps running to completion
// B: it is cancelled automatically when the view disappears
// C: it is cancelled only if you store it and call cancel()
  • A
  • B
  • C

Mental model. onAppear is a doorbell that can ring many times. task is an errand you send out when someone enters the room, called back automatically when they leave.

onAppear runs when a view is inserted into the view hierarchy that is on screen, and onDisappear when it is removed. It can run more than once for the same view (a tab being re-selected, navigation back, a list row scrolling in again), so it must be safe to repeat, and it is not a once-only “did load” hook. Its closure is synchronous, which makes it a poor place for async work. .task { await … } is the right tool: it starts an async task when the view appears and cancels it automatically when the view disappears, and structured cancellation propagates into the awaited calls. .task(id: value) also cancels and restarts it when value changes, which is the clean way to “reload when the query changes” without manual bookkeeping.

.onChange(of: value) { old, new in … } runs when a value changes between updates, for reacting with side effects (saving, validating, starting a request). Since iOS 17 the action receives old and new values and can use initial: true to also run on first appearance. It is not a replacement for deriving UI from state: use it for effects, not to copy state from one variable to another. Keep side effects out of body. Compared with UIKit, there is no single “view loaded” moment, and a view struct’s init runs on every parent update and so must be free of work; state initialised with @State in init is created once and kept. For the app-level lifecycle see the scene-phase question, and for how SwiftUI’s hooks map onto UIKit’s, see the Topic 6 lifecycle question.

The task is tied to the view’s lifetime. Cancellation is cooperative, so the awaited code must check for it, and well-behaved async APIs do.

Common traps

  • Starting a Task { } in onAppear and never cancelling it. It outlives the view; use .task.
  • Doing expensive work in the view’s init. It runs on every parent update.

They might follow up with

  • How do you reload when a search term changes? .task(id: term), which cancels the old request and starts a new one.
  • Can onAppear fire twice? Yes, so keep it idempotent.

Related questions

  • 8.4.2 What is the App protocol, what are scenes, and what does scenePhase tell you?
  • 8.2.1 How does SwiftUI decide to re-run body, and how do you keep updates cheap?
  • 6.2.4 How does the SwiftUI lifecycle map onto UIKit’s, and what is scenePhase for?

What is the App protocol, what are scenes, and what does scenePhase… →