SwiftUI : NavigationStack in new iOS 18 TabView pushes twice when path in parameter

Hello,

With iOS 18, when NavigationStack is in new TabView, with path parameter containing current navigation state is set, the navigation destination view is pushed twice.

See below with example that pushes twice on iOS 18 but is correct on iOS 17

@MainActor
class NavigationModel: ObservableObject {
    static let shared = NavigationModel()
    
    @Published var selectedTab: String
    @Published var homePath: [Route]
    @Published var testPath: [Route]
}


struct ContentView: View {
    @StateObject private var navigationModel: NavigationModel = NavigationModel.shared
    
    var body: some View {
        TabView(selection: $navigationModel.selectedTab){
            HomeView()
                .tabItem {
                    Label("Home", systemImage: "house")
                }
                .tag("home")
            
            TestView()
                .tabItem {
                    Label("Test", systemImage: "circle")
                }
                .tag("test")
        }
    }
}


struct HomeView: View {
    @StateObject private var navigationModel: NavigationModel = NavigationModel.shared

    var body: some View {
        NavigationStack(path: $navigationModel.homePath){
            VStack{
                Text("home")
                NavigationLink(value: Route.test1("test1")){
                    Text("Go to test1")
                }
            }
            .navigationDestination(for: Route.self){ route in
                NavigationModelBuilder.findFinalDestination(route:route)
            }
        }
    }
}

I don't what causes the issue because it works well on iOS 16 and iOS 17. I think the path is somehow reset but I don't why (maybe by the TabView ?)

Note that the bug only occurs with TabView. Don't really know if it is a TabView bug or if it is on my side.

I filed a feedback with sample project FB14312064

Answered by brebispanique in 799115022

The issue seems to be fixed in iOS 18 beta 5

I am seeing the same thing. App that works fine on 17.5 is quite broken on 18. I also notice that after navigating back and forth a few times the whole navigation stack seems corrupted. Old views that you have previously popped re-appear in the wrong context in the navigation stack, and views are not completely drawn

Thanks for submitting a bug report @brebispanique.

While the team looks into the Feedback report, Trying making these changes to your sample project and let me know if you're still able to reproduce the issue:

  • You could also use the Route enum instance itself for , since it's hashable and get rid of the custom implementation.
enum Route: Hashable {
    case test1(String)
    case test2(String)
    case empty(String)
    
    var id: Self {
        self
    }
}
  • HomeView and TestView should use ObservedObject property wrapper since you are passing a StateObject into a subview.
struct TestView: View {
    @ObservedObject var navigationModel: NavigationModel
    ......

}


struct HomeView: View {
    @ObservedObject var navigationModel: NavigationModel
.....
    }

I'll also suggest you consider migrating from the Observable Object protocol to the Observable macro as well.

I have found a workaround using NavigationPath instead of an array of Route in my NavigationModel

class NavigationModel: ObservableObject {
    static let shared = NavigationModel()
    
    @Published var selectedTab: String
    @Published var homePath: NavigationPath
    @Published var testPath: NavigationPath
}

Edit: It fixes the "push twice" issue but NavigationStack still acts weirdly, even though the path is correct, some views are not correctly popped.

Removing the TabView still fixes all issues so I think the TabView is buggy.

I can confirm that everything works correct when using the NavigationStack outside a TabView. Inside a TabView, even if there is only a single tab, exhibits incorrect behaviour

I'm also facing this issue. I didn't know the TabView was what was causing it. Thought I was going crazy. @DTS Engineer When do you think this could be fixed?

I also face similar issue and wrote about it in another post: https://developer.apple.com/forums/thread/760041

I can confirm that the usage NavigationPath fixes this issue but the other issues appear. A views that weren't pushed appear in path and I have to pop the several times. So it seems that the combination TabView + NavigationStack inside is buggy.

Two notes:

  1. I tried to use iOS 17+ @Observable approach. It didn’t help.
  2. Using @State var path: [RouterDestination] = [] directly inside View seems to help. But it is not what I want as I need this property to be @Published and located inside custom Router class where I can get an access to it, and use for programmatic navigation if needed.

FYI I am also able to reproduce the issue with latest iOS 18 beta 4.

I can confirm that the issue IS NOT fixed in iOS 18 beta 4 Simulator (22A5316j).

Accepted Answer

The issue seems to be fixed in iOS 18 beta 5

Only tried on my iPad as yet, and it does seem to be fixed in beta 5

@brebispanique @sjb_s it is not fixed on my side, unfortunately. I can see the same issue on Simulator.

Do you test on a real device or simulator?

Could you please attach a full code of your solution so I'll be able to compile it and check?

Not fixed on my side either. My path is a @Published variable in an ObservableObject. Used to work fine in iOS 17

This is a content view for a test app that had the issue pre-beta 5, but works fine on my iPad with beta 5

import SwiftUI

class Item: Identifiable, Hashable {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    static func == (lhs: Item, rhs: Item) -> Bool {
        lhs.name == rhs.name
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
}

class Model: ObservableObject {
    @Published var path = [Item]()
}

struct ContentView: View {
    var items = [
        Item(name: "Item 1"),
        Item(name: "Item 2"),
        Item(name: "Item 3"),
    ]
    
    @StateObject private var model = Model()
    
    var body: some View {
        TabView {
            Text("Go to Tab 2")
                .tabItem { Label("Tab 1", systemImage: "storefront") }
                .tag("1")
            tab2()
                .tabItem { Label("Tab 2", systemImage: "globe") }
                .tag("2")
        }
    }
    
    @ViewBuilder
    func tab2() -> some View {
        NavigationStack(path: $model.path) {
            List(items) { item in
                NavigationLink(item.name, value: item)
            }
            .navigationDestination(for: Item.self) { item in
                DetailView(item: item)
            }
        }
    }
}

struct DetailView: View {
    var item: Item
    
    var body: some View {
        Text("Details...\(item.name)")
    }
}

#Preview {
    ContentView()
}

@sjb_s @dderg @brebispanique @DTS Engineer Here is the full code (a modification of @sjb_s 's example) that still DOESN'T work well.

The main reason is that navigationDestination is located in View extension. It is how it is designed in my real app where I have a single navigationDestination for the whole app and all possible routes inside it.

Everything works well in iOS 17.x with this approach, but still doesn't work well in iOS 18.0 Beta 5.

Any ideas?

import SwiftUI

class Item: Identifiable, Hashable {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    static func == (lhs: Item, rhs: Item) -> Bool {
        lhs.name == rhs.name
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
    }
}

class Model: ObservableObject {
    @Published var path = [Item]()
}

struct ContentView: View {
    var items = [
        Item(name: "Item 1"),
        Item(name: "Item 2"),
        Item(name: "Item 3"),
    ]
    
    @StateObject private var model = Model()
    
    var body: some View {
        TabView {
            Text("Go to Tab 2")
                .tabItem { Label("Tab 1", systemImage: "storefront") }
                .tag("1")
            tab2()
                .tabItem { Label("Tab 2", systemImage: "globe") }
                .tag("2")
        }
    }
    
    @ViewBuilder
    func tab2() -> some View {
        NavigationStack(path: $model.path) {
            List(items) { item in
                NavigationLink(item.name, value: item)
            }
            .withAppRouter()
        }
    }
}

extension View {
    func withAppRouter() -> some View {
        navigationDestination(for: Item.self) { item in
            DetailView(item: item)
        }
    }
}

struct DetailView: View {
    var item: Item
    
    var body: some View {
        Text("Details...\(item.name)")
    }
}

@DTS Engineer I created and send Bug report in Feedback assistant: https://feedbackassistant.apple.com/feedback/14743917

SwiftUI : NavigationStack in new iOS 18 TabView pushes twice when path in parameter
 
 
Q