iosswiftswiftuiswiftui-tabviewswiftui-navigationstack

Hiding new tab view in iOS 18 when pushing screens


With iOS 18 Apple decided to move our beloved bottom tab bar controller to the top of the screen (apparently thats more modern). I am having the following situation: I have TabView where we have root screen FirstView, which has a button wrapped in NavigationStack. When tapping on the button we push a second view where we want the tab bar hidden. However because the first screen has a title when pushing the view jumps.

https://imgur.com/a/TdYwqZp

Here is the code:

struct ContentView: View {
    @State var path: [String] = []
    @State var hideTabBar: Bool = false

    var body: some View {
        TabView {
            navigationStack
                .tabItem {
                    Text("Eng")
                }
            placeholderStack
                .tabItem {
                    Text("Span")
                }
        }
    }

    var navigationStack: some View {
        NavigationStack(path: $path) {
            Text("First View")
                .navigationTitle("English View")
                .navigationDestination(for: String.self) {
                    Text($0)
                        .toolbar {
                            ToolbarItem(placement: .principal) {
                                Text("Second View")
                            }
                        }
                }
                .onTapGesture {
                    hideTabBar = true
                    path.append("Second View")
                }
                .onAppear {
                    hideTabBar = false
                }
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        Text("Top Bar")
                    }
                }
                .toolbar(hideTabBar ? .hidden : .visible, for: .tabBar)
        }
    }
    
    var placeholderStack: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hola, mundo")
        }
        .padding()
    }
}


Solution

  • When navigating to the child view, it works better if the tab bar is hidden first, then navigation is performed afterwards. This can be done by setting the flag using withAnimation and then performing the navigation in a completion callback.

    For navigating back, the transition can be made a bit smoother by resetting the flag for hiding the tab bar in .onDisappear for the child view, with animation:

    Text("First View")
        .navigationTitle("English View")
        .navigationDestination(for: String.self) {
            Text($0)
                .toolbar {
                    ToolbarItem(placement: .principal) {
                        Text("Second View")
                    }
                }
                .onDisappear {
                    withAnimation {
                        hideTabBar = false
                    }
                }
        }
        .onTapGesture {
            withAnimation {
                hideTabBar = true
            } completion: {
                path.append("Second View")
            }
        }
        .toolbar {
            ToolbarItem(placement: .principal) {
                Text("Top Bar")
            }
        }
        .toolbar(hideTabBar ? .hidden : .visible, for: .tabBar)
    

    Animation


    Another way of navigating back is to replace the native back button with a custom one. This allows the flag for the tab bar to be reset first, then navigation can be performed afterwards, like before.

    This approach avoids the staggered animation when navigating back. However, you lose the effect of the large navigation title morphing into the back button. It also seems to be important to set .navigationBarTitleDisplayMode(.large), otherwise the navigation title doesn't show when returning to the parent view.

    Text("First View")
        .navigationBarTitleDisplayMode(.large)
        .navigationTitle("English View")
        .navigationDestination(for: String.self) {
            Text($0)
                .navigationBarBackButtonHidden()
                .toolbar {
                    ToolbarItem(placement: .navigation) {
                        Button {
                            withAnimation {
                                hideTabBar = false
                            } completion: {
                                path.removeLast()
                            }
                        } label: {
                            HStack {
                                Image(systemName: "chevron.left")
                                    .fontWeight(.semibold)
                                    .imageScale(.large)
                                Text("English View")
                            }
                        }
                    }
                    ToolbarItem(placement: .principal) {
                        Text("Second View")
                    }
                }
        }
        .onTapGesture {
            withAnimation {
                hideTabBar = true
            } completion: {
                path.append("Second View")
            }
        }
        .toolbar {
            ToolbarItem(placement: .principal) {
                Text("Top Bar")
            }
        }
        .toolbar(hideTabBar ? .hidden : .visible, for: .tabBar)
    

    Animation