pawello2222
pawello2222

Reputation: 54426

SwiftUI Hide TabView bar inside NavigationLink views

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?

Upvotes: 20

Views: 18124

Answers (7)

wong
wong

Reputation: 1

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)
        }
    }
}

Upvotes: 0

Xenolion
Xenolion

Reputation: 12717

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)
    }

Upvotes: 6

Brotsky Engineer
Brotsky Engineer

Reputation: 41

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
                
            }
          
        }

Upvotes: 0

Asperi
Asperi

Reputation: 257493

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.

Upvotes: 13

Jon_the_developer
Jon_the_developer

Reputation: 338

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)
    }
}

Upvotes: 3

Hikeland
Hikeland

Reputation: 416

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.

Upvotes: 19

pawello2222
pawello2222

Reputation: 54426

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("-")
        }
    }
}

Upvotes: 7

Related Questions