SwiftUI:在NavigationLink视图中隐藏TabView栏

16

我有一个TabView,每个Tab项都有单独的NavigationView堆栈。它运行良好,但当我打开任何NavigationView时,TabView栏仍然显示。我希望无论何时点击任何NavigationView时,它都会消失。

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

我找到了一个解决方案,可以将TabView放在NavigationView中,这样当我单击NavigationView上的导航链接后,TabView栏会隐藏起来。但是这会破坏选项卡项目的NavigationBarTitles。

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

使用这种解决方案,实现每个TabView项目的不同NavigationTabBars的唯一方法是使用嵌套的NavigationView。也许有一种正确实现嵌套NavigationView的方法?(据我所知,导航层次结构中应该只有一个NavigationView)。

在SwiftUI中,如何正确隐藏NavigationLink视图中的TabView栏?


如何在SwiftUI中使用NavigationLink时隐藏TabBar? - rbaldwin
@rbaldwin 这个解决方案会破坏NavigationBar的标题。我希望每个选项卡都有不同的NavigationBar标题。使用TabView在NavigationView中,我只能设置一次NavigationBar标题。 - pawello2222
7个回答

15

我非常喜欢上面发布的解决方法,但我不喜欢TabBar在视图转换时没有隐藏。

实际上,当你向左滑动以使用tabBar.isHidden导航返回时,结果是无法接受的。

我决定放弃原生的SwiftUI TabView并编写自己的代码。结果在UI方面更美观:

iPhone 模拟器

这是用于实现此结果的代码:

首先,定义一些视图:

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))
    }
}
然后创建TabBarView(它将成为您的应用程序中使用的根视图):
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
        }
    }
}
此解决方案还允许在选项卡栏中进行许多自定义设置。例如,您可以添加一些通知标记。

很棒的想法,谢谢,真的帮了我很大的忙! - Alex Hartford
这很有帮助。我稍微调整了一下,并将zstack包装在NavigationView中,以便我可以从TabBarView直接推送到全屏视图,因此在导航方面,thirdview实际上是tabbarview的兄弟姐妹(就像在UIKIt中一样)。通过这样做,我也不需要在何时或何时不将tabbarview放入zstack中进行检查。 - Teffi
太好了,你真的救了我的一天。 - Tim Chiang
这对我来说是不可接受的。因为你正在使用if-else来显示视图。当你切换选项卡时,整个视图将再次重新渲染。不再可重用。 - Binh Ho
@BinhHo 不,你可以将视图保留在一个变量中。 - Hikeland
显示剩余2条评论

12
如果我们谈论标准的TabView,可能的解决方法可以基于我在Programmatically detect Tab Bar or TabView height in SwiftUI中的TabBarAccessor答案。这里是在包含NavigationView的选项卡项目中所需的修改。测试过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
        })
    }
}

注意:当然,如果FirstTabView应该是可重用的并且可以独立实例化,则应该将内部的tabBar属性设置为可选,并明确处理不存在的tabBar。


4
它能够正常运行,但并不完美。这种方法会使得在我从子页面返回时,选项卡栏只有在整个过渡动画完成后才会出现。但如果找不到其他方法的话,我将使用它。 - pawello2222
7
我没找到更好的解决方案,所以接受了你的回答。我只是不明白为什么没有更好的方法来做这件事。感觉我的应用程序层次结构相当标准。 - pawello2222

6

多亏了 Asperi 的答案,我能找到一种不会破坏动画效果并且看起来自然的解决方案。

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

动态计算navigationBarTitlenavigationBarItems

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

我计划使用您的方法,将TabView放置在NavigationView中。在SwiftUI 2和iOS14中,您是否遇到过此方法的任何问题? - Gary
1
@Gary 不,对我来说很好用。你总是可以自己测试一下 - 上面的代码可以直接复制粘贴使用。如果你的目标是iOS14+,你可能只需要将.navigationBarTitle更改为.navigationTitle - pawello2222
我确实在模拟器中测试过它,而且它也起作用了 :) 我之所以还要问是因为一些其他网站,比如hackingwithswift说在NavigationView中放置TabView是一个坏主意(例如https://www.hackingwithswift.com/forums/swiftui/ios-14-recommendations-for-tabview-and-navigationview/3998),所以我很好奇当你的项目变得越来越大和复杂时,是否会遇到任何问题。听起来你没有遇到任何问题,所以我也会采用这种方法。 - Gary
2
此解决方案不适用于iOS 15.2。导航栏的行为出现了意外情况。 - Andre

5

iOS 16 有更新,现在可以隐藏任何导航栏。在这种情况下:

NavigationLink("Click") {
        Text("Next View")
            .toolbar(.hidden, for: .tabBar)
    }

3

好的,

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

0

同时,您还可以为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
                
            }
          
        }

0

我也遇到了这个问题。我不想重写,但解决方案在我的Github上。我在那里详细地写了一切。 https://github.com/BrotskyS/AdvancedNavigationWithTabView

P.S:我没有声望来写评论。Hikeland的解决方案还不错。但是你不能保存页面的状态。如果你有一个ScrollView,在你改变标签时它会每次都重置为零。


我确认这个不起作用。顶部有很大的空间。IMG_3B30A255308A-1 - Binh Ho
1
我认为这是因为您将整个应用程序都包装在NavigationView中。请删除它,仅将每个屏幕包装在TabView中。 - Brotsky Engineer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接