SwiftUI Hide TabView bar inside NavigationLink views
Asked Answered
T

7

19

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?

Thermoelectrometer answered 23/5, 2020 at 10:55 Comment(2)
#57305376 ?Showing
@Showing 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.Thermoelectrometer
T
13

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.

Talent answered 23/5, 2020 at 11:57 Comment(2)
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.Thermoelectrometer
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.Thermoelectrometer
H
17

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.

Hbomb answered 29/11, 2020 at 10:11 Comment(7)
Great idea, thanks, really helped me out!Resign
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.Ares
Has anyone tried this and was able to preserve the view's state. I'm having issues similar to this #57772637Ares
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 scaleSkiles
It's so nice, you really save my day.Rubino
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.Beal
@BinhHo no, you can retain the views in a var.Hbomb
T
13

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.

Talent answered 23/5, 2020 at 11:57 Comment(2)
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.Thermoelectrometer
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.Thermoelectrometer
T
7

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("-")
        }
    }
}
Thermoelectrometer answered 27/7, 2020 at 14:48 Comment(4)
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?Piecemeal
@Piecemeal 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.Thermoelectrometer
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 hackingwithswift.com/forums/swiftui/…), 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.Piecemeal
This solution is not appropriate for use in iOS 15.2. The navigation bar behaves unexpectedly.Tagore
Y
6

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)
    }
Yi answered 26/4, 2023 at 17:36 Comment(0)
Z
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)
    }
}
Zebapda answered 18/3, 2021 at 20:18 Comment(0)
P
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
                
            }
          
        }
Phillisphilly answered 11/10, 2022 at 15:20 Comment(0)
R
0

After IOS 16+, we can store the navigationTile's value as a @State.

struct AppTabView: View {
    @State private var showOnboarding: Bool = true
    @State private var selection: Int = 0
    @State private var navigationTitle: String = "Home"
    @State private var navigationBarVisibility: Visibility = .hidden
    @State private var navigationBarTitleDisplayMode: NavigationBarItem.TitleDisplayMode = .inline
    
    @State var isDailyWallpaperIndexPresented: Bool = false
    @State var isBatteryMoodIndexPresented: Bool = false

    init() {
//        UITabBar.appearance().backgroundColor = .white
    }
    var body: some View {
        NavigationStack {
            TabView(selection: $selection) {
                
                HomeView(isDailyWallpaperIndexPresented: $isDailyWallpaperIndexPresented, isBatteryMoodIndexPresented: $isBatteryMoodIndexPresented)
                    .tabItem {
                        Image(systemName: "house")
                        Text("Home")
                    }.tag(0)
                    .onWillAppear {
                        navigationTitle = "Home"
                        navigationBarVisibility = .hidden
                    }
                
                SettingView()
                    .tabItem {
                        Image(systemName: "ellipsis")
                        Text("About")
                    }.tag(1)
                    .onWillAppear {
                        navigationTitle = "Setting"
                        navigationBarVisibility = .visible
                    }
                Text("abc")
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(.green)
                    .tabItem {
                        Image(systemName: "42.circle")
                        Text(String(selection))
                    }.tag(2)
                    .onWillAppear {
                        navigationTitle = "Home"
                        navigationBarVisibility = .hidden
                    }
            }
            .fullScreenCover(isPresented: $showOnboarding, content: {
                OnboardingView(showOnboarding: $showOnboarding)
            })
            .navigationTitle(navigationTitle)
            .navigationBarTitleDisplayMode(navigationBarTitleDisplayMode)
            .toolbar(navigationBarVisibility, for: .navigationBar)
            
            .navigationDestination(isPresented: $isDailyWallpaperIndexPresented) {
                DailyWallpaperIndexView()
            }
            .navigationDestination(isPresented: $isBatteryMoodIndexPresented) {
                BatteryMoodIndexView()
            }
        }
    }
}

struct SettingView: View {
    @State var xPosition: CGFloat = 0
    
    var body: some View {
        List {
            Section {
                
                NavigationLink {
                    Label("About", systemImage: "info.circle")
                } label: {
                    Label("About", systemImage: "info.circle")
                }
                NavigationLink {
                    Label("Privacy", systemImage: "hand.raised.circle")
                } label: {
                    Label("Privacy", systemImage: "hand.raised.circle")
                }
            }
            Section {
                
                NavigationLink {
                    ZStack {
                        Color.blue.ignoresSafeArea()
                        Label("Help", systemImage: "questionmark.circle")
                    }
                } label: {
                    Label("Help", systemImage: "questionmark.circle")
                }
            }
            HStack {
                Spacer()
                VersionView()
                Spacer()
            }.listRowBackground(Color.clear)
        }
    }
}
Robbyrobbyn answered 27/3 at 1:38 Comment(1)
TabView should be at the top, each tab gets its own NavigationStackJejune

© 2022 - 2024 — McMap. All rights reserved.