5
votes

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.

2
I did something similar in here, but there is a SwiftUI internal crash in workflow. I observe same crash with your code snapshot. Just be aware.Asperi
@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
@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
Provided my approachAsperi

2 Answers

2
votes

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

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