简体   繁体   English

如何在 SwiftUI 中使用 NavigationView 创建自定义 TabView?

[英]How to create a custom TabView with NavigationView in SwiftUI?

[EDIT] - This question has been edited and simplified. [编辑] - 这个问题已经过编辑和简化。

I need to create a CustomLooking TabView instead of the default one.我需要创建一个 CustomLooking TabView 而不是默认的。

Here is my full code with the problem.这是我解决问题的完整代码。 Just run the code below.只需运行下面的代码。

import SwiftUI

enum TabName {
    case explore, network
}

struct ContentView: View {
    @State var displayedTab: TabName = .explore
    var body: some View {
        VStack(spacing: 0) {
            Spacer()
            switch displayedTab {
            case .explore: AViewWhichNavigates(title: "Explore").background(Color.yellow)
            case .network: AViewWhichNavigates(title: "Network").background(Color.green)
            }
            Spacer()
            CustomTabView(displayedTab: $displayedTab)
        }
    }
}

struct CustomTabView: View {
    @Binding var displayedTab: TabName
    var body: some View {
        HStack {
            Spacer()
            Text("Explore").border(Color.black, width: 1).onTapGesture { self.displayedTab = .explore }
            Spacer()
            Text("Network").border(Color.black, width: 1).onTapGesture { self.displayedTab = .network }
            Spacer()
        }
    }
}

struct AViewWhichNavigates: View {
    let title: String
    var body: some View {
        NavigationView(content: {
            NavigationLink(destination: Text("We are one level deep in navigation")) {
                Text("You are at root. Tap to navigate").navigationTitle(title)
            }
        })
    }
}

On tab#1 click the navigation.在 tab#1 上单击导航。 Switch to tab#2, then Switch back to tab#1.切换到标签#2,然后切换回标签#1。 You will see that tab#1 has popped to root.您将看到 tab#1 已弹出到根目录。

How do I prevent the customTabView from popping to root every time i switch tabs?每次切换标签时,如何防止 customTabView 弹出到根目录?

The problem is that the Navigation isActive state is not recorded as well as the displayed tab state.问题是未记录 Navigation isActive state 以及显示的选项卡 state。

By recording the state of the navigation of each tab as well as which tab is active the correct navigation state can be show for each tab.通过记录每个选项卡导航的 state 以及哪个选项卡处于活动状态,可以为每个选项卡显示正确的导航 state。

The model can be improved to remove the tuple and make it more flexible but the key thing is the use of getter and setter to use an encapsulated model of what the navigation state is for each tab in order to allow the NavigationLink to update it via a binding.可以改进 model 以删除元组并使其更灵活,但关键是使用 getter 和 setter 来使用封装的 model 的导航 Z9ED39E2EA931586B6A985A6942EF5 允许通过每个选项卡更新到导航链接以将其更新为捆绑。

I have simplified the top level VStack and removed the top level switch as its not needed here, but it can be added back for using different types of views at the top level in a real implementation我已经简化了顶层 VStack 并删除了顶层开关,因为这里不需要它,但可以添加回来以便在实际实现中在顶层使用不同类型的视图


enum TabName : String {
    case Explore, Network
}

struct ContentView: View {
    
    @State var model =  TabModel()
    
    init(){
         UINavigationBar.setAnimationsEnabled(false)
    }
    
    var body: some View {
        VStack(spacing: 0) {
            Spacer()
            AViewWhichNavigates(model: $model).background(Color.green)
            Spacer()
            CustomTabView(model:$model)
        }
    }
}

struct CustomTabView: View {
    @Binding var model: TabModel
    var body: some View {
        HStack {
            Spacer()
            Text("Explore").border(Color.black, width: 1).onTapGesture { model.selectedTab = .Explore }
            Spacer()
            Text("Network").border(Color.black, width: 1).onTapGesture { model.selectedTab = .Network }
            Spacer()
        }
    }
}

struct AViewWhichNavigates: View {
    @Binding var model:TabModel
    
    var body: some View {
        NavigationView(content: {
            NavigationLink(destination: Text("We are one level deep in navigation in \(model.selectedTab.rawValue)"), isActive: $model.isActive) {
                Text("You are at root of \(model.selectedTab.rawValue). Tap to navigate").navigationTitle(model.selectedTab.rawValue)
            }.onDisappear {
                UINavigationBar.setAnimationsEnabled(model.isActive)
            }
        })
    }
}

struct TabModel {
    var selectedTab:TabName = .Explore
    var isActive : Bool {
        get {
            switch selectedTab {
                case .Explore : return tabMap.0
                case .Network : return tabMap.1
            }
        }
        set {
            switch selectedTab {
                case .Explore : nOn(isActive, newValue); tabMap.0 = newValue;
                case .Network : nOn(isActive, newValue); tabMap.1 = newValue;
            }
        }
    }
    //tuple used to represent a fixed set of tab isActive navigation states
    var tabMap = (false, false)
     
    func  nOn(_ old:Bool,_ new:Bool ){
        UINavigationBar.setAnimationsEnabled(new && !old)
    }
    
}

All you need is a ZStack with opacity.你所需要的只是一个不透明的 ZStack。

import SwiftUI

enum TabName {
    case explore, network
}

struct ContentView: View {
    @State var displayedTab: TabName = .explore
    var body: some View {
        VStack {
            ZStack {
                AViewWhichNavigates(title: "Explore")
                    .background(Color.green)
                    .opacity(displayedTab == .explore ? 1 : 0)
                AViewWhichNavigates(title: "Network")
                    .background(Color.green)
                    .opacity(displayedTab == .network ? 1 : 0)
            }
            CustomTabView(displayedTab: $displayedTab)
        }
    }
}

struct CustomTabView: View {
    @Binding var displayedTab: TabName
    var body: some View {
        HStack {
            Spacer()
            Text("Explore").border(Color.black, width: 1).onTapGesture { self.displayedTab = .explore }
            Spacer()
            Text("Network").border(Color.black, width: 1).onTapGesture { self.displayedTab = .network }
            Spacer()
        }
    }
}

struct AViewWhichNavigates: View {
    let title: String
    var body: some View {
        NavigationView(content: {
            NavigationLink(destination: Text("We are one level deep in navigation")) {
                Text("You are at root. Tap to navigate").navigationTitle(title)
            }
        })
    }
}

I think it is possible even with your custom tab view, because the issue is in rebuilding ExploreTab() when you switch tabs, so all content of that tab is rebuilt as well, so internal NavigationView on rebuilt is on first page.我认为即使使用您的自定义选项卡视图也是可能的,因为问题在于在切换选项卡时重建ExploreTab() ,因此该选项卡的所有内容也会被重建,因此重建时的内部NavigationView位于第一页。

Assuming you have only one ExploreTab in your app (as should be obvious), the possible solution is to make it Equatable explicitly and do not allow SwiftUI to replace it on refresh.假设您的应用程序中只有一个ExploreTab (应该很明显),可能的解决方案是使其显式Equatable并且不允许 SwiftUI 在刷新时替换它。

So所以

struct ExploreTab: View, Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        return true    // prevent replacing ever !!
    }

    var body: some View {
       // ... your code here
    }
}

and

    VStack(spacing: 0) {
        switch displayedTab {
        case .explore: ExploreTab().equatable()      // << here !!
        case .network: NetworkTab()
        }
        CustomTabView(displayedTab: $displayedTab)   //This is the Custom TabBar
    }

Update: tested with Xcode 12 / iOS 14 - works as described above (actually the same idea works for standard containers)更新:用 Xcode 12 / iOS 14 测试- 如上所述工作(实际上相同的想法适用于标准容器)

Here is a quick demo replication of CustomTabView with test environment as described above.这是使用上述测试环境的CustomTabView的快速演示复制。

演示

Full module code:完整的模块代码:

struct ExploreTab: View, Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        return true    // prevent replacing ever !!
    }

    var body: some View {
       NavigationView {
            NavigationLink("Go", destination: Text("Explore"))
       }
    }
}

enum TestTabs {
    case explore
    case network
}

struct CustomTabView: View {
    @Binding var displayedTab: TestTabs
    var body: some View {
        HStack {
            Button("Explore") { displayedTab = .explore }
            Divider()
            Button("Network") { displayedTab = .network }
        }
        .frame(maxWidth: .infinity)
        .frame(height: 80).background(Color.yellow)
    }
}

struct TestCustomTabView: View {
    @State private var displayedTab = TestTabs.explore

    var body: some View {
        VStack(spacing: 0) {
            switch displayedTab {
                case .explore: ExploreTab().equatable()      // << here !!
                case .network: Text("NetworkTab").frame(maxWidth: .infinity, maxHeight: .infinity)
            }
            CustomTabView(displayedTab: $displayedTab)   //This is the Custom TabBar
        }

        .edgesIgnoringSafeArea(.bottom)
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM