onAppear fires twice — or never when you expect it. How does it differ from UIKit's lifecycle?
This screen loads its feed in onAppear, but the network fetch runs more than once — the list ends up with duplicated items after navigating away and back.
Requirements:
- Explain why
onAppearcan run more than once for the same screen. - Make the load happen once and cancel itself when the view goes away.
struct FeedView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { RowView(item: $0) }
.onAppear { Task { items = try await api.loadFeed() } } // runs again on re-insert
}
}
Find and fix the bug.
onAppear follows the view's structural identity, not a controller lifecycle, and fires each time the view enters the hierarchy. A re-inserted view calls it again; one never inserted never fires. It is not a once-per-screen viewDidAppear.
- ✗Treating
onAppearas a once-per-screenviewDidAppearequivalent - ✗Putting idempotency-sensitive setup in
onAppearwithout a guard - ✗Expecting
onAppearto fire for a view that is never inserted
- →Why does a re-identified view fire
onAppearagain? - →Where would you put load-once logic so a re-insertion doesn't repeat it?
onAppear is not viewDidAppear. It is tied to the view's structural identity and fires every time that view is inserted into the render tree — navigating away and back re-inserts FeedView, so onAppear runs again and appends a second copy of the feed. It can also never run for a view that is never inserted (behind a false condition or below the fold of a lazy container).
Prefer .task, which starts when the view appears and is automatically cancelled when it disappears — and give it an id so it does not repeat unless the identity you key on changes:
struct FeedView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { RowView(item: $0) }
.task { // auto-cancelled on disappear
guard items.isEmpty else { return }
items = (try? await api.loadFeed()) ?? []
}
}
}
If you must stay on onAppear, guard the work with a stored flag or move ownership of the loaded data into a model that survives re-insertion. The rule: never assume onAppear runs exactly once.