5

Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.

But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:

import SwiftUI

enum Gender: String {
    case female, male
}

let members: [Gender: [String]] = [
    Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]

struct TabItem: View {
    let image: String
    let label: String
    var body: some View {
        VStack {
            Image(systemName: image).imageScale(.large)
            Text(label)
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            TabView {
                ListView(gender: .female).tag(0).tabItem {
                    TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
                }
                ListView(gender: .male).tag(1).tabItem {
                    TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
                }
            }
        }
    }
}

struct ListView: View {
    let gender: Gender
    var body: some View {
        let names = members[gender]!
        return List {
            ForEach(0..<names.count, id: \.self) { index in
                NavigationLink(destination: DetailView(name: names[index])) {
                    Text(names[index])
                }
            }
        }.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
    }
}

struct DetailView: View {
    let name: String
    var body: some View {
        ZStack {
            VStack {
                Text("profile views")
            }
            VStack {
                Spacer()
                HStack {
                    Spacer()
                    TabItem(image: "pencil.circle", label: "Edit")
                    Spacer()
                    TabItem(image: "minus.circle", label: "Delete")
                    Spacer()
                }
            }
        }
        .navigationBarTitle(Text(name), displayMode: .inline)
    }
}

What I could do is to have a @State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.

ZhouX
  • 1,866
  • 18
  • 22
  • I did something similar in [here](https://stackoverflow.com/a/59016082/12299030), but there is a SwiftUI internal crash in workflow. I observe same crash with your code snapshot. Just be aware. – Asperi Jan 02 '20 at 07:02
  • @Asperi, you are right, simulator give warnings while switching lists and dismissing detail views, but app is still there and working. But in real phone it crashes.. Thanks for the heads up! Have you figured out how to get rid of those errors? or any way to hide tab view dynamically? – ZhouX Jan 02 '20 at 07:29
  • @Asperi, I guess the tab view stands in the way somehow breaks the connection of navigation root view and its child views, that is why title cant be set from child views, and also why child views have nowhere to go back to. – ZhouX Jan 02 '20 at 07:40
  • Provided my approach – Asperi Jan 02 '20 at 08:09

2 Answers2

2

The idea is to join TabView selection with NavigationView content dynamically.

Demo:

TabView with NavigationView

Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.

struct TestTabsOverNavigation: View {

    @State private var tabVisible = true
    @State private var selectedTab: Int = 0
    var body: some View {
        ZStack(alignment: .bottom) {
            contentView
            tabBar
        }
    }

    var contentView: some View {
        NavigationView {
            ListView(gender: selectedTab == 0 ? .female : .male)
            .onAppear {
                withAnimation {
                    self.tabVisible = true
                }
            }
            .onDisappear {
                withAnimation {
                    self.tabVisible = false
                }
            }
        }
    }

    var tabBar: some View {
        TabView(selection: $selectedTab) {
            Rectangle().fill(Color.clear).tag(0).tabItem {
                TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
            }
            Rectangle().fill(Color.clear).tag(1).tabItem {
                TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
            }
        }
        .frame(height: 50) // << !! might be platform dependent
        .opacity(tabVisible ? 1.0 : 0.0)
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    LOL, this is exactly what I am doing right now! Just got stuck at where you put double exclamation marks above, I am trying to find a way to get tab bar default height from system instead of hard coding. – ZhouX Jan 02 '20 at 08:24
  • it has opaque background that is why I limit its height. – Asperi Jan 02 '20 at 08:29
  • the opaque background might be the only thing not 100% perfect yet. The background of tab view should be same with the navigation bar, a bit blurred look and feel. You can put like 30 more names into that members dict and set listRowBackground to Color.red in list view then scroll down a bit to make list item below navigation bar, to compare the backgrounds of those two views. I tried to move TabView out of NavigationView and directly hold the list view, I can see two backgrounds are same, but with the ZStack solution, tab view background is 100% opaque. – ZhouX Jan 02 '20 at 11:05
1

This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:

@State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
    NavigationView{
        TabView(selection: $selectedTab, content:{
            HomeView()
                .tabItem {
                    Image(systemName: "house.fill")
                    Text(pageTitles[0])
                }.tag(0)
            CustomerListView()
                .tabItem {
                    Image(systemName: "rectangle.stack.person.crop.fill")
                    Text(pageTitles[1])
                }.tag(1)
            SaleView()
                .tabItem {
                    Image(systemName: "tag.fill")
                    Text(pageTitles[2])
                }.tag(2)
            
            MoreView()
                .tabItem {
                    Image(systemName: "ellipsis.circle.fill")
                    Text(pageTitles[3])
                }.tag(3)
        })
        .navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)

        .font(.headline)
    }
}
Nguyen Minh Binh
  • 23,891
  • 30
  • 115
  • 165