16

I have a TabView and separate NavigationView stacks for every Tab item. It works well but when I open any NavigationLink the TabView bar is still displayed. I'd like it to disappear whenever I click on any NavigationLink.

struct MainView: View {
    @State private var tabSelection = 0

    var body: some View {
        TabView(selection: $tabSelection) {
            FirstView()
                .tabItem {
                    Text("1")
                }
                .tag(0)
            SecondView()
                .tabItem {
                    Text("2")
                }
                .tag(1)
        }
    }
}

struct FirstView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: FirstChildView()) { // How can I open FirstViewChild with the TabView bar hidden?
                Text("Go to...")
            }
            .navigationBarTitle("FirstTitle", displayMode: .inline)
        }
    }
}

I found a solution to put a TabView inside a NavigationView, so then after I click on a NavigationLink the TabView bar is hidden. But this messes up NavigationBarTitles for Tab items.

struct MainView: View {
    @State private var tabSelection = 0

    var body: some View {
        NavigationView {
            TabView(selection: $tabSelection) {
                ...
            }
        }
    }
}

struct FirstView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: FirstChildView()) {
                Text("Go to...")
            }
            .navigationBarTitle("FirstTitle", displayMode: .inline) // This will not work now
        }
    }
}

With this solution the only way to have different NavigationTabBars per TabView item, is to use nested NavigationViews. Maybe there is a way to implement nested NavigationViews correctly? (As far as I know there should be only one NavigationView in Navigation hierarchy).

How can I hide TabView bar inside NavigationLink views correctly in SwiftUI?

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • https://stackoverflow.com/questions/57304876/how-to-hide-the-tabbar-when-navigate-with-navigationlink-in-swiftui ? – rbaldwin May 23 '20 at 11:29
  • @rbaldwin This solution messes up NavigationBar titles. I'd like to have every tab item to have a different NavigationBar title. With a TabView inside a NavigationView I can set NavigationBar title only once. – pawello2222 May 23 '20 at 13:15

7 Answers7

15

I really enjoyed the solutions posted above, but I don't like the fact that the TabBar is not hiding according to the view transition. In practice, when you swipe left to navigate back when using tabBar.isHidden, the result is not acceptable.

I decided to give up the native SwiftUI TabView and code my own. The result is more beautiful in the UI:

iPhone Simulator

Here is the code used to reach this result:

First, define some views:

struct FirstView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("First View")
                    .font(.headline)
            }
            .navigationTitle("First title")
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
            .background(Color.yellow)
        }
    }
}

struct SecondView: View {
    var body: some View {
        VStack {
            NavigationLink(destination: ThirdView()) {
                Text("Second View, tap to navigate")
                    .font(.headline)
            }
        }
        .navigationTitle("Second title")
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
        .background(Color.orange)
    }
}

struct ThirdView: View {
    var body: some View {
        VStack {
            Text("Third View with tabBar hidden")
                .font(.headline)
        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
        .background(Color.red.edgesIgnoringSafeArea(.bottom))
    }
}

Then, create the TabBarView (which will be the root view used in your app):

struct TabBarView: View {
    enum Tab: Int {
        case first, second
    }
    
    @State private var selectedTab = Tab.first
    
    var body: some View {
        VStack(spacing: 0) {
            ZStack {
                if selectedTab == .first {
                    FirstView()
                }
                else if selectedTab == .second {
                    NavigationView {
                        VStack(spacing: 0) {
                            SecondView()
                            tabBarView
                        }
                    }
                }
            }
            .animation(nil)
            
            if selectedTab != .second {
                tabBarView
            }
        }
    }
    
    var tabBarView: some View {
        VStack(spacing: 0) {
            Divider()
            
            HStack(spacing: 20) {
                tabBarItem(.first, title: "First", icon: "hare", selectedIcon: "hare.fill")
                tabBarItem(.second, title: "Second", icon: "tortoise", selectedIcon: "tortoise.fill")
            }
            .padding(.top, 8)
        }
        .frame(height: 50)
        .background(Color.white.edgesIgnoringSafeArea(.all))
    }
    
    func tabBarItem(_ tab: Tab, title: String, icon: String, selectedIcon: String) -> some View {
        ZStack(alignment: .topTrailing) {
            VStack(spacing: 3) {
                VStack {
                    Image(systemName: (selectedTab == tab ? selectedIcon : icon))
                        .font(.system(size: 24))
                        .foregroundColor(selectedTab == tab ? .primary : .black)
                }
                .frame(width: 55, height: 28)
                
                Text(title)
                    .font(.system(size: 11))
                    .foregroundColor(selectedTab == tab ? .primary : .black)
            }
        }
        .frame(width: 65, height: 42)
        .onTapGesture {
            selectedTab = tab
        }
    }
}

This solution also allows a lot of customization in the TabBar. You can add some notifications badges, for example.

Hikeland
  • 357
  • 4
  • 7
  • Great idea, thanks, really helped me out! – Alex Hartford Nov 02 '21 at 00:54
  • This was helpful. I tweaked it a little bit and wrapped zstack inside a NavigationView so that I can push to the full screen view right from TabBarView so in terms of navigation, thirdview is actually a sibling of tabbarview (like in UIKIt). With that I also don't need to place check when or when not to add tabbarview in zstack. – Teffi Feb 10 '22 at 07:04
  • Has anyone tried this and was able to preserve the view's state. I'm having issues similar to this https://stackoverflow.com/questions/57772137/tabview-resets-navigation-stack-when-switching-tabs – Teffi Mar 24 '22 at 13:05
  • It works nice and smooth! But you need to set the logic to every view. If you have more complex hierarchy of screens it's almost impossible to scale – Nizami May 03 '22 at 10:56
  • It's so nice, you really save my day. – Tim Chiang Jul 16 '22 at 11:27
  • This is not acceptable for me. Because you are using if-else to display view. When you change tab, whole view will be re-rendered again. Not reusable anymore. – Binh Ho Nov 11 '22 at 10:27
  • @BinhHo no, you can retain the views in a var. – Hikeland Nov 12 '22 at 11:44
12

If we talk about standard TabView, the possible workaround solution can be based on TabBarAccessor from my answer on Programmatically detect Tab Bar or TabView height in SwiftUI

Here is a required modification in tab item holding NavigationView. Tested with Xcode 11.4 / iOS 13.4

demo

struct FirstTabView: View {
    @State private var tabBar: UITabBar! = nil

    var body: some View {
        NavigationView {
            NavigationLink(destination:
                FirstChildView()
                    .onAppear { self.tabBar.isHidden = true }     // !!
                    .onDisappear { self.tabBar.isHidden = false } // !!
            ) {
                Text("Go to...")
            }
            .navigationBarTitle("FirstTitle", displayMode: .inline)
        }
        .background(TabBarAccessor { tabbar in   // << here !!
            self.tabBar = tabbar
        })
    }
}

Note: or course if FirstTabView should be reusable and can be instantiated standalone, then tabBar property inside should be made optional and handle ansbsent tabBar explicitly.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 4
    It works but it's not perfect. This way when I return from the child view the tab bar appears only after the whole transition animation completes. But I'll use it if I don't find any other way. – pawello2222 May 23 '20 at 13:06
  • 7
    I didn't find any better solution, so I'm accepting your answer. I just don't understand why there's no better way to do it. It feels like my app's hierarchy is pretty standard. – pawello2222 May 26 '20 at 20:54
6

Thanks to another Asperi's answer I was able to find a solution which does not break animations and looks natural.

struct ContentView: View {
    @State private var tabSelection = 1

    var body: some View {
        NavigationView {
            TabView(selection: $tabSelection) {
                FirstView()
                    .tabItem {
                        Text("1")
                    }
                    .tag(1)
                SecondView()
                    .tabItem {
                        Text("2")
                    }
                    .tag(2)
            }
            // global, for all child views
            .navigationBarTitle(Text(navigationBarTitle), displayMode: .inline)
            .navigationBarHidden(navigationBarHidden)
            .navigationBarItems(leading: navigationBarLeadingItems, trailing: navigationBarTrailingItems)
        }
    }
}
struct FirstView: View {
    var body: some View {
        NavigationLink(destination: Text("Some detail link")) {
            Text("Go to...")
        }
    }
}

struct SecondView: View {
    var body: some View {
        Text("We are in the SecondView")
    }
}

Compute navigationBarTitle and navigationBarItems dynamically:

private extension ContentView {
    var navigationBarTitle: String {
        tabSelection == 1 ? "FirstView" : "SecondView"
    }
    
    var navigationBarHidden: Bool {
        tabSelection == 3
    }

    @ViewBuilder
    var navigationBarLeadingItems: some View {
        if tabSelection == 1 {
            Text("+")
        }
    }

    @ViewBuilder
    var navigationBarTrailingItems: some View {
        if tabSelection == 1 {
            Text("-")
        }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • I am planning to use your approach of putting the TabView inside a NavigationView. Have you run into any issues with this approach in SwiftUI 2 in iOS14? – Gary Oct 29 '20 at 22:57
  • 1
    @Gary Nope, works fine for me. You can always test it yourself - the above code is copy-paste-ready. If you're targeting iOS14+ you may just want to change `.navigationBarTitle` to `.navigationTitle`. – pawello2222 Oct 29 '20 at 23:01
  • I did actually test it in the simulator and it worked :) The reason I still asked is because some other sites such as hackingwithswift say it is a bad idea to put a TabView inside a NavigationView (example https://www.hackingwithswift.com/forums/swiftui/ios-14-recommendations-for-tabview-and-navigationview/3998), and so I was curious if you ran into any bugs with this approach as your project got bigger and more complex. It sounds like you have not had any issues with it, so I will also go with this approach. – Gary Oct 29 '20 at 23:31
  • 2
    This solution is not appropriate for use in iOS 15.2. The navigation bar behaves unexpectedly. – Andre Jan 16 '22 at 04:16
3

How about,

struct TabSelectionView: View {
    @State private var currentTab: Tab = .Scan
    
    private enum Tab: String {
        case Scan, Validate, Settings
    }
    
    var body: some View {
        TabView(selection: $currentTab){
            
            ScanView()
                .tabItem {
                    Label(Tab.Scan.rawValue, systemImage: "square.and.pencil")
                }
                .tag(Tab.Scan)
            
            ValidateView()
                .tabItem {
                    Label(Tab.Validate.rawValue, systemImage: "list.dash")
                }
                .tag(Tab.Validate)
            
            SettingsView()
                .tabItem {
                    Label(Tab.Settings.rawValue, systemImage: "list.dash")
                }
                .tag(Tab.Settings)
        }
        .navigationBarTitle(Text(currentTab.rawValue), displayMode: .inline)
    }
}
3

There is an Update for iOS 16, you can now hide any of the Navigation Bars. In this case:

NavigationLink("Click") {
        Text("Next View")
            .toolbar(.hidden, for: .tabBar)
    }
Xenolion
  • 12,035
  • 7
  • 33
  • 48
0

I also faced this problem. I don't want to rewrite, but the solution is in my github. I wrote everything in detail there https://github.com/BrotskyS/AdvancedNavigationWithTabView

P.S: I have no reputation to write comments. Hikeland's solution is not bad. But you do not save the State of the page. If you have a ScrollView, it will reset to zero every time when you change tab

  • I confirmed this not work. Huge space at top. ![IMG_3B30A255308A-1](https://user-images.githubusercontent.com/9253378/201325554-97d257b8-3453-45ab-9817-e4368596e2a2.jpeg) – Binh Ho Nov 11 '22 at 10:52
  • 1
    I think because you wrapped all your app in NavigationView. Delete it and wrap only each screen in tabView – Brotsky Engineer Nov 29 '22 at 11:32
0

Also you can create very similar custom navBar for views in TabView

struct CustomNavBarView<Content>: View where Content: View {
var title: String = ""
let content: Content

init(title: String, @ViewBuilder content: () -> Content) {
    self.title = title
    self.content = content()
}
var body: some View {
    content
        .safeAreaInset(edge: .top, content: {
            HStack{
                Spacer()
                Text(title)
                    .fontWeight(.semibold)
                Spacer()
            }
            .padding(.bottom, 10)
            .frame(height: 40)
            .frame(maxWidth: .infinity)
            .background(.ultraThinMaterial)
            .overlay {
                Divider()
                    .frame(maxHeight: .infinity, alignment: .bottom)
            }
        })
}
}



  CustomNavBarView(title: "Create ad"){
            ZStack{
               
                NavigationLink(destination: SetPinMapView(currentRegion: $vm.region, region: vm.region), isActive: $vm.showFullMap) {
                    Color.clear
                }
                
                Color("Background").ignoresSafeArea()
                
                content
                
            }
          
        }