Task, onAppear, onDisappear modifiers run twice

I've run into an issue with my app that I've been able to narrow down to a small reproducer.

Any time there is a task associated with the DetailView and you "pop to top", onAppear is called again and the task is re-run. Why is that? Is this a SwiftUI bug? It doesn't happen on iOS 17, only 18.

import SwiftUI

@Observable
class Store {
	var shown: Bool = true
}

@main
struct MyApp: App {
	@State private var store = Store()
	
	var body: some Scene {
		WindowGroup {
			if store.shown {
				ContentView()
			} else {
				EmptyView()
			}
		}
		.environment(store)
	}
}

struct ContentView: View {
	var body: some View {
		NavigationView {
			NavigationLink(destination: DetailView()) {
				Text("Go to Detail View")
			}
		}
	}
}

struct DetailView: View {
	@Environment(Store.self) private var store
	
	init() {
		print("DetailView initialized")
	}
	
	var body: some View {
		Button("Pop to top") {
			store.shown = false
		}
		.task {
			print("DetailView task executed")
		}
		.onAppear {
			print("DetailView appeared")
		}
		.onDisappear {
			print("DetailView disappeared")
		}
	}
}

I could not test on iOS 17 (my sample project crashes Xcode 15.3). Do you confirm it works there ?

I see the same on iOS 18:

DetailView initialized
DetailView appeared
DetailView task executed
-->> button tapped
DetailView disappeared
DetailView appeared
DetailView disappeared
DetailView task executed

This discussion may be interesting to read, even it did not let me understand what happens: https://fatbobman.com/en/posts/mastering_swiftui_task_modifier/

I was able to reproduce this on iOS 17 by animating the view transitions by changing the top level MyApp to:

@main
struct MyApp: App {
	@State private var store = Store()
	
	var body: some Scene {
		WindowGroup {
			if store.shown {
				ContentView()
					.transition(.move(edge: .bottom))
			} else {
				EmptyView()
					.transition(.move(edge: .bottom))
			}
		}
		.environment(store)
	}
}

and the DetailView to:

struct DetailView: View {
	@Environment(Store.self) private var store
	
	var body: some View {
		Button("Pop to top") {
			withAnimation {
				store.shown = false
			}
		}
		.task {
			print("DetailView task executed")
		}
		.onAppear {
			print("DetailView appeared")
		}
		.onDisappear {
			print("DetailView disappeared")
		}
	}
}

By the way, this only happens in this simple reproducer if the DetailView is shown from the ContentView within the NavigationView.

The difference between the output results in a completely different structural identity:

if store.shown {
	ContentView()
} else {
	EmptyView()
}

and

if store.shown {
	ContentView()
}

The rendered results are the same, but the identity of the composed views differ. The former is a _ConditionalContent<TrueView,FalseView>, while the latter is Optional<View>.

Please submit a bug report regarding this issue using Feedback Assistant (https://feedbackassistant.apple.com) and post the Feedback ID Number here for the record.

Task, onAppear, onDisappear modifiers run twice
 
 
Q