Navigation issue in Swiftui iOS 18 skipping a page

I have an app that takes an ObservableObject known as AppState(), sets up as StateObject and is passed through to MenuView() and so on as an EnvironmentObject from the @main struct.

My MenuView makes use of the new TabView with TabSection and Tab. This is nested in a NavigationStack with navigationDestination() inside it and .alert() attached to the outside of it.

The primary use case for this attached Alert is to navigate to a specific page from ANY view within the app.

ContentView() navigates to PageTwo() by use of navigationDestination() which binds to an @EnvironmentObject updated within a Task (the actual app performs async operations, so this needs to be kept). Note that NavigationStack is also in this view and not the others.

On PageTwo(), navigating to PageThree() navigate the same way as before but will also start a timer for 10 seconds.

Once the timer is up, an alert will popup. This gives the option to navigate to the ThirdPage() again. This seems to work well from any View inside the app.

The issue I am running into though is, once navigation to PageThree() has occurred VIA THE ALERT, pressing the Back button in the top left will skip PageTwo() and go straight back to ContentView().

Undesired behaviour.

Is there something I am doing wrong? Am I setting up the navigation incorrectly? Have I handled the navigationDestinations incorrectly?

Min repro:

@main
struct FirstAppApp: App {
    
    @StateObject private var app = AppState()
    
    var body: some Scene {
        WindowGroup {
            MenuView()
                .environmentObject(app)
        }
    }
}

struct MenuView: View {
    
    @EnvironmentObject var app: AppState
    @State private var device = UIDevice.current.userInterfaceIdiom
    @AppStorage("MyAppTabViewCustomization")
    private var customisation: TabViewCustomization
    
    var body: some View {
        NavigationStack {
            Group {
                if app.showDisclaimer {
                    Legal()
                } else {
                    TabView {
                        ForEach(SidebarItem.allCases, id: .self) { tab in
                            TabSection("Planning") {
                                Tab("Plan", systemImage: "house") {
                                    ContentView()
                                }.customizationID("plan")
                            }
                        }
                    }
                    .tabViewStyle(.sidebarAdaptable)
                    .tabViewCustomization($customisation)
                }
            }
            .navigationDestination(isPresented: $app.atisUpdated) {
                PageThree()
            }
        }
        .alert(isPresented: $app.newAtisAvailable) {
            Alert(
                title: Text("Press GO to navigate to page 3"),
                primaryButton: .default(Text("GO"), action: {
                    Task {
                        app.atisUpdated = true
                    }
                }),
                secondaryButton: .cancel(Text("STOP"), action: {
                    Task {
                        app.atisTimer.invalidate()
                    }
                })
            )
        }
    }
}

enum SidebarItem: String, Identifiable, CaseIterable {
    case planner = "Planner"
    var id: String { self.rawValue }
    var iconName: String {
        switch self {
        case .planner:
            return "house"
        }
    }
    var customizationID: String {
        switch self {
        case .planner:
            return "planner"
        }
    }
}


struct ContentView: View {
    
    @EnvironmentObject var app: AppState
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Page 1")
                Button("Go to page 2") {
                    Task {
                        app.readyToNavigate = true
                    }
                }
            }
            .navigationDestination(isPresented: $app.readyToNavigate) {
                PageTwo()
            }
        }
    }
}


struct PageTwo: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 2")
            Button("Go to page 3") {
                Task {
                    app.atisButtonPressed = true
                    await app.startTimer()
                }
            }
        }
        .navigationDestination(isPresented: $app.atisButtonPressed) {
            PageThree()
        }
    }
}


struct PageThree: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Page 3")
            }
        }
    }
}



class AppState: ObservableObject {
    @Published var showDisclaimer: Bool = false // !UserDefaults.standard.bool(forKey: "DisclaimerAccepted")
    @Published var atisButtonPressed: Bool = false
    @Published var readyToNavigate: Bool = false
    @Published var atisUpdated: Bool = false
    @Published var atisTimer: Timer = Timer()
    @Published var newAtisAvailable: Bool = false

    @MainActor
    func startTimer() async {
        self.atisTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
            self.newAtisAvailable = true
        }
    }
}

5

The issue is because you have multiple NavigationStack.

If you need more control over the path of the NavigationStack
try using NavigationStack(path: $navigationPath) { ....} and
change the path in your Views accordingly.

Here is my example code that works for me.

struct MenuView: View {
    @EnvironmentObject var app: AppState
    
    @State private var device = UIDevice.current.userInterfaceIdiom
    @AppStorage("MyAppTabViewCustomization")
    private var customisation: TabViewCustomization
    
    var body: some View {
        Group {
            if app.showDisclaimer {
                Legal()
            } else {
                TabView {
                    ForEach(SidebarItem.allCases, id: .self) { tab in
                        TabSection("Planning") {
                            Tab("Plan", systemImage: "house") {
                                ContentView()
                            }.customizationID("plan")
                        }
                    }
                }
                .tabViewStyle(.sidebarAdaptable)
                .tabViewCustomization($customisation)
            }
        }
        .alert(isPresented: $app.newAtisAvailable) {
            Alert(
                title: Text("Press GO to navigate to page 3"),
                primaryButton: .default(Text("GO"), action: {
                    app.atisUpdated = true
                }),
                secondaryButton: .cancel(Text("STOP"), action: {
                    app.atisTimer.invalidate()
                })
            )
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Page 1")
                Button("Go to page 2") {
                    app.readyToNavigate = true
                }
            }
            .navigationDestination(isPresented: $app.readyToNavigate) {
                PageTwo()
            }
        }
    }
}


struct PageTwo: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 2")
            Button("Go to page 3") {
                Task {
                    app.atisButtonPressed = true
                    await app.startTimer()
                }
            }
        }
        .navigationDestination(isPresented: $app.atisButtonPressed) {
            PageThree()
        }
    }
}


struct PageThree: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 3")
        }
    }
}

struct Legal: View {
    var body: some View {
        Text("Legal")
    }
}

Note, the forums link you show in your comment does not say ...navstack in each view,
it says NavigationStack for each Tab (that needs it). The meaning is
NavigationStack should not be the top most View, it/they should be inside the TabView. Note also, you will have to adjust your .alert(...) code as I don’t really understand what you want to achieve with it.

EDIT-1:

An alternative approach is to use NavigationStack(path: $app.path) {...} as
shown in this example code:

struct MenuView: View {
    @EnvironmentObject var app: AppState
    
    @State private var device = UIDevice.current.userInterfaceIdiom
    @AppStorage("MyAppTabViewCustomization")
    private var customisation: TabViewCustomization
    
    var body: some View {
        Group {
            if app.showDisclaimer {
                Legal()
            } else {
                TabView {
                    ForEach(SidebarItem.allCases, id: .self) { tab in
                        TabSection("Planning") {
                            Tab("Plan", systemImage: "house") {
                                ContentView()
                            }.customizationID("plan")
                        }
                    }
                }
                .tabViewStyle(.sidebarAdaptable)
                .tabViewCustomization($customisation)
            }
        }
        .alert(isPresented: $app.newAtisAvailable) {
            Alert(
                title: Text("Press GO to navigate to page 3"),
                primaryButton: .default(Text("GO"), action: {
                    app.atisUpdated = true
                    app.path.append("Page3")  // <--- here
                }),
                secondaryButton: .cancel(Text("STOP"), action: {
                    app.atisTimer.invalidate()
                })
            )
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        NavigationStack(path: $app.path) {  // <--- here
            VStack {
                Text("Page 1")
                Button("Go to page 2") {
                    app.readyToNavigate = true
                    app.path.append("Page2")  // <--- here
                }
            }
            .navigationDestination(for: String.self) { page in  // <--- here
                if page == "Page2" {
                    PageTwo()
                } else if page == "Page3" {
                    PageThree()
                }
            }
        }
    }
}

struct PageTwo: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 2")
            Button("Go to page 3") {
                app.atisButtonPressed = true
                app.path.append("Page3")  // <--- here
                Task {
                    await app.startTimer()
                }
            }
        }
    }
}

struct PageThree: View {
    @EnvironmentObject var app: AppState
    
    var body: some View {
        VStack {
            Text("Page 3")
        }
    }
}

struct Legal: View {
    var body: some View {
        Text("Legal")
    }
}

class AppState: ObservableObject {
    @Published var showDisclaimer: Bool = false // !UserDefaults.standard.bool(forKey: "DisclaimerAccepted")
    @Published var atisButtonPressed: Bool = false
    @Published var readyToNavigate: Bool = false
    @Published var atisUpdated: Bool = false
    @Published var atisTimer: Timer = Timer()
    @Published var newAtisAvailable: Bool = false
    
    @Published var path = NavigationPath() // <--- here
    
    @MainActor
    func startTimer() async {
        self.atisTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
            self.newAtisAvailable = true
        }
    }
}

1

I think the problem is that you'r not ensuring that NavStack maintains the correct  state, the root cause is that the navigation state might be overwritten or improperly managed when the alert triggers the navigation.

You must ensure single NavStack in the entire App rather than nesting NavStacks instances within individual views, then Use Binding and ObservableObject properly, you must manage the navigation state more effectively by ensuring state changes are correctly bound and observed across views.

here is an update of your version code : 

import SwiftUI

@main
struct FirstAppApp: App {
    @StateObject private var app = AppState()

    var body: some Scene {
        WindowGroup {
            NavigationStack {
                MenuView()
                    .environmentObject(app)
            }
        }
    }
}

struct MenuView: View {
    @EnvironmentObject var app: AppState
    @AppStorage("MyAppTabViewCustomization") private var customisation: TabViewCustomization

    var body: some View {
        Group {
            if app.showDisclaimer {
                Legal()
            } else {
                TabView {
                    ForEach(SidebarItem.allCases, id: .self) { tab in
                        TabSection("Planning") {
                            Tab("Plan", systemImage: "house") {
                                ContentView()
                            }.customizationID("plan")
                        }
                    }
                }
                .tabViewStyle(.sidebarAdaptable)
                .tabViewCustomization($customisation)
                .alert(isPresented: $app.newAtisAvailable) {
                    Alert(
                        title: Text("Press GO to navigate to page 3"),
                        primaryButton: .default(Text("GO"), action: {
                            app.atisUpdated = true
                        }),
                        secondaryButton: .cancel(Text("STOP"), action: {
                            app.atisTimer.invalidate()
                        })
                    )
                }
            }
        }
        .navigationDestination(isPresented: $app.atisUpdated) {
            PageThree()
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var app: AppState

    var body: some View {
        VStack {
            Text("Page 1")
            Button("Go to page 2") {
                app.readyToNavigate = true
            }
        }
        .navigationDestination(isPresented: $app.readyToNavigate) {
            PageTwo()
        }
    }
}

struct PageTwo: View {
    @EnvironmentObject var app: AppState

    var body: some View {
        VStack {
            Text("Page 2")
            Button("Go to page 3") {
                app.atisButtonPressed = true
                app.startTimer()
            }
        }
        .navigationDestination(isPresented: $app.atisButtonPressed) {
            PageThree()
        }
    }
}

struct PageThree: View {
    @EnvironmentObject var app: AppState

    var body: some View {
        VStack {
            Text("Page 3")
        }
    }
}

class AppState: ObservableObject {
    @Published var showDisclaimer: Bool = false // !UserDefaults.standard.bool(forKey: "DisclaimerAccepted")
    @Published var atisButtonPressed: Bool = false
    @Published var readyToNavigate: Bool = false
    @Published var atisUpdated: Bool = false
    @Published var atisTimer: Timer = Timer()
    @Published var newAtisAvailable: Bool = false

    func startTimer() {
        self.atisTimer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { _ in
            DispatchQueue.main.async {
                self.newAtisAvailable = true
            }
        }
    }
}

New contributor

Hussein is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật