I'm trying to show a fullScreenCover when the user taps the third tab instead of actually switching to that tab. The issue is that resetting selectedTab = oldValue in onChange breaks the NavigationStack push/pop animations in the previous tab.
The Problem
When I reset the tab selection synchronously, the NavigationStack in the previous tab loses its animations - no push/pop transitions work anymore until I switch tabs away and back.
Broken code:
struct ContentView: View {
@State private var selectedTab: Int = 0
@State private var showSheet: Bool = false
var body: some View {
TabView(selection: $selectedTab) {
Tab("First", systemImage: "1.circle.fill", value: 0) {
FirstTabView()
}
Tab("Second", systemImage: "2.circle.fill", value: 1) {
SecondTabView()
}
Tab("Sheet", systemImage: "ellipsis", value: 2, role:.search) {
EmptyView()
}
}
.onChange(of: selectedTab) { oldValue, newValue in
if newValue == 2 {
showSheet = true
selectedTab = oldValue // This breaks NavigationStack animations!
}
}
.fullScreenCover(isPresented: $showSheet) {
SheetView()
}
}
}
Broken navigation animation here: https://youtube.com/shorts/SeBlTQxbV68
The Workaround
Adding a small delay before resetting the tab selection seems to fix it:
.onChange(of: selectedTab) { oldValue, newValue in
if newValue == 2 {
Task { @MainActor in
showSheet = true
try? await Task.sleep(for: .seconds(0.25))
selectedTab = oldValue
}
}
}
Working with delay: https://youtube.com/shorts/B4AbX72vc3g
Full Reproducible Code
import SwiftUI
struct FirstTabView: View {
var body: some View {
NavigationStack {
VStack {
Text("Basic View")
}
}
}
}
struct SecondTabView: View {
@State private var items: [String] = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]
var body: some View {
NavigationStack {
List(items, id: \.self) { item in
NavigationLink(value: item) {
Text(item)
}
}
.navigationTitle("Second Tab")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: String.self) { item in
Text(item)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
items.append("Item \(items.count + 1)")
}) {
Image(systemName: "plus")
}
}
}
}
}
}
struct SheetView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
VStack {
Text("Hello World")
}
.navigationTitle("Sheet View")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
dismiss()
}) {
Image(systemName: "xmark")
}
}
}
}
}
}
struct ContentView: View {
@State private var selectedTab: Int = 0
@State private var showSheet: Bool = false
var body: some View {
TabView(selection: $selectedTab) {
Tab("First", systemImage: "1.circle.fill", value: 0) {
FirstTabView()
}
Tab("Second", systemImage: "2.circle.fill", value: 1) {
SecondTabView()
}
Tab("Sheet", systemImage: "ellipsis", value: 2, role:.search) {
EmptyView()
}
}
.onChange(of: selectedTab) { oldValue, newValue in
if newValue == 2 {
Task { @MainActor in
showSheet = true
try? await Task.sleep(for: .seconds(0.25))
selectedTab = oldValue
}
}
}
.fullScreenCover(isPresented: $showSheet) {
SheetView()
}
}
}
#Preview {
ContentView()
}
Questions
- Why does the synchronous reset break NavigationStack animations?
- Is there a cleaner solution that doesn't require a hardcoded delay?
- Is this a known iOS 26 bug with TabView and NavigationStack?
Environment: iOS 26.1, Xcode 26.1
.searchrole is intended for tab where users search. It does not mean "a button on the bottom right".