【问题标题】:SwiftUI: Custom Tab View for macOS & iOSSwiftUI:macOS 和 iOS 的自定义选项卡视图
【发布时间】:2023-03-18 19:49:01
【问题描述】:

有没有一种简单的方法可以使用 SwiftUI 获得更可定制的标签栏视图?我主要是从 macOS 的角度询问(尽管适用于任何系统的系统都是理想的),因为标准系统的 macOS 实现存在各种问题:

  • 它周围有一个圆形边框,这意味着它在子视图中使用任何类型的背景颜色都很糟糕。
  • 它不支持标签图标。
  • 在定制方面非常有限。
  • 有问题(有时无法按预期切换视图)。
  • 看起来很陈旧。

当前代码:

import SwiftUI

struct SimpleTabView: View {

    @State private var selection = 0

    var body: some View {

        TabView(selection: $selection) {

            HStack {
                Spacer()
                VStack {
                    Spacer()
                    Text("First Tab!")
                    Spacer()
                }
                Spacer()
            }
                .background(Color.blue)
                .tabItem {
                    VStack {
                        Image("icons.general.home")
                        Text("Tab 1")
                    }
                }
                .tag(0)

            HStack {
                Spacer()
                VStack {
                    Spacer()
                    Text("Second Tab!")
                    Spacer()
                }
                Spacer()
            }
                .background(Color.red)
                .tabItem {
                    VStack {
                        Image("icons.general.list")
                        Text("Tab 2")
                    }
                }
                .tag(1)

            HStack {
                Spacer()
                VStack {
                    Spacer()
                    Text("Third Tab!")
                    Spacer()
                }
                Spacer()
            }
                .background(Color.yellow)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .tabItem {
                    VStack {
                        Image("icons.general.cog")
                        Text("Tab 3")
                    }
                }
                .tag(2)
        }
    }
}

【问题讨论】:

    标签: macos swiftui tabview nstabview


    【解决方案1】:

    为了解决这个问题,我整理了以下简单的自定义视图,它提供了与 iOS 更相似的选项卡界面,即使在 Mac 上运行时也是如此。它只需要一组元组就可以工作,每个元组都概述了选项卡的标题、图标名称和内容。

    它可以在 Light & Dark 模式下运行,并且可以在 macOS iOS / iPadOS / 等上运行,但您可能只想在运行时使用标准的 TabView 实现IOS;由你决定。

    它还包含一个参数,因此您可以根据偏好将栏放置在顶部或底部(顶部更适合 macOS 指南)。

    这是一个结果示例(在暗模式下):

    这是代码。一些注意事项:

    • 它使用Color 的基本扩展,因此它可以使用系统背景颜色,而不是硬编码。
    • 唯一有点奇怪的部分是额外的 backgroundshadow 修饰符,它们是防止 SwiftUI 将阴影应用到每个子视图(!)所必需的。当然,如果您不想要阴影,您可以删除所有这些行(包括 zIndex)。

    Swift v5.1:

    import SwiftUI
    
    public extension Color {
    
        #if os(macOS)
        static let backgroundColor = Color(NSColor.windowBackgroundColor)
        static let secondaryBackgroundColor = Color(NSColor.controlBackgroundColor)
        #else
        static let backgroundColor = Color(UIColor.systemBackground)
        static let secondaryBackgroundColor = Color(UIColor.secondarySystemBackground)
        #endif
    }
    
    public struct CustomTabView: View {
        
        public enum TabBarPosition { // Where the tab bar will be located within the view
            case top
            case bottom
        }
        
        private let tabBarPosition: TabBarPosition
        private let tabText: [String]
        private let tabIconNames: [String]
        private let tabViews: [AnyView]
        
        @State private var selection = 0
        
        public init(tabBarPosition: TabBarPosition, content: [(tabText: String, tabIconName: String, view: AnyView)]) {
            self.tabBarPosition = tabBarPosition
            self.tabText = content.map{ $0.tabText }
            self.tabIconNames = content.map{ $0.tabIconName }
            self.tabViews = content.map{ $0.view }
        }
        
        public var tabBar: some View {
            
            HStack {
                Spacer()
                ForEach(0..<tabText.count) { index in
                    HStack {
                        Image(self.tabIconNames[index])
                        Text(self.tabText[index])
                    }
                    .padding()
                    .foregroundColor(self.selection == index ? Color.accentColor : Color.primary)
                    .background(Color.secondaryBackgroundColor)
                    .onTapGesture {
                        self.selection = index
                    }
                }
                Spacer()
            }
            .padding(0)
            .background(Color.secondaryBackgroundColor) // Extra background layer to reset the shadow and stop it applying to every sub-view
            .shadow(color: Color.clear, radius: 0, x: 0, y: 0)
            .background(Color.secondaryBackgroundColor)
            .shadow(
                color: Color.black.opacity(0.25),
                radius: 3,
                x: 0,
                y: tabBarPosition == .top ? 1 : -1
            )
            .zIndex(99) // Raised so that shadow is visible above view backgrounds
        }
        public var body: some View {
            
            VStack(spacing: 0) {
                
                if (self.tabBarPosition == .top) {
                    tabBar
                }
                
                tabViews[selection]
                    .padding(0)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                
                if (self.tabBarPosition == .bottom) {
                    tabBar
                }
            }
            .padding(0)
        }
    }
    

    这里有一个例子来说明你如何使用它。显然,您也可以将一个完全自定义的子视图传递给它,而不是像这样动态构建它们。只需确保将它们包装在 AnyView 初始化程序中即可。

    图标及其名称是自定义的,因此您必须使用自己的替代品。

    struct ContentView: View {
        
        var body: some View {
            CustomTabView(
                tabBarPosition: .top,
                content: [
                    (
                        tabText: "Tab 1",
                        tabIconName: "icons.general.home",
                        view: AnyView(
                            HStack {
                                Spacer()
                                VStack {
                                    Spacer()
                                    Text("First Tab!")
                                    Spacer()
                                }
                                Spacer()
                            }
                            .background(Color.blue)
                        )
                    ),
                    (
                        tabText: "Tab 2",
                        tabIconName: "icons.general.list",
                        view: AnyView(
                            HStack {
                                Spacer()
                                VStack {
                                    Spacer()
                                    Text("Second Tab!")
                                    Spacer()
                                }
                                Spacer()
                            }
                            .background(Color.red)
                        )
                    ),
                    (
                        tabText: "Tab 3",
                        tabIconName: "icons.general.cog",
                        view: AnyView(
                            HStack {
                                Spacer()
                                VStack {
                                    Spacer()
                                    Text("Third Tab!")
                                    Spacer()
                                }
                                Spacer()
                            }
                            .background(Color.yellow)
                        )
                    )
                ]
            )
        }
    }
    

    【讨论】:

    • 由于您只需在每个选项卡上用@State 替换 tabViews 数组中的 tabView,因此您的 tabView 的视图将被重新渲染。与系统提供的 TabView 不同,它拥有视图并且不会在更改时重新渲染。但实际上,如果我们想使用自定义 TabBar,我找不到比这更好的解决方案。
    • 感谢您的意见。是的,关于视图重新渲染,这是一个很好的观点,并且同意,这并不理想。使用泛型和@ViewBuilder 可能会变得更简单(就像我在这个例子中所做的stackoverflow.com/a/63679567/2272431),但我不确定这会带来巨大的好处。希望 macOS 有朝一日能够支持改进的标签视图!
    【解决方案2】:

    回复 TheNeil(我没有足够的声誉来添加评论):

    我喜欢你的解决方案,稍微修改了一下。

    public struct CustomTabView: View {
        
        public enum TabBarPosition { // Where the tab bar will be located within the view
            case top
            case bottom
        }
        
        private let tabBarPosition: TabBarPosition
        private let tabText: [String]
        private let tabIconNames: [String]
        private let tabViews: [AnyView]
        
        @State private var selection = 0
        
            public init(tabBarPosition: TabBarPosition, content: [(tabText: String, tabIconName: String, view: AnyView)]) {
            self.tabBarPosition = tabBarPosition
            self.tabText = content.map{ $0.tabText }
            self.tabIconNames = content.map{ $0.tabIconName }
            self.tabViews = content.map{ $0.view }
            }
        
            public var tabBar: some View {
            VStack {
                Spacer()
                    .frame(height: 5.0)
                HStack {
                    Spacer()
                        .frame(width: 50)
                    ForEach(0..<tabText.count) { index in
                        VStack {
                            Image(systemName: self.tabIconNames[index])
                                .font(.system(size: 40))
                            Text(self.tabText[index])
                        }
                        .frame(width: 65, height: 65)
                        .padding(5)
                        .foregroundColor(self.selection == index ? Color.accentColor : Color.primary)
                        .background(Color.secondaryBackgroundColor)
                        .onTapGesture {
                            self.selection = index
                        }
                        .overlay(
                            RoundedRectangle(cornerRadius: 25)
                                .fill(self.selection == index ? Color.backgroundColor.opacity(0.33) : Color.red.opacity(0.0))
                        )               .onTapGesture {
                            self.selection = index
                        }
    
                    }
                    Spacer()
                }
                .frame(alignment: .leading)
                .padding(0)
                .background(Color.secondaryBackgroundColor) // Extra background layer to reset the shadow and stop it applying to every sub-view
                .shadow(color: Color.clear, radius: 0, x: 0, y: 0)
                .background(Color.secondaryBackgroundColor)
                .shadow(
                    color: Color.black.opacity(0.25),
                    radius: 3,
                    x: 0,
                    y: tabBarPosition == .top ? 1 : -1
                )
                .zIndex(99) // Raised so that shadow is visible above view backgrounds
            }
            }
    
        public var body: some View {
            VStack(spacing: 0) {
                    if (self.tabBarPosition == .top) {
                    tabBar
                }
            
                tabViews[selection]
                .padding(0)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
            
                if (self.tabBarPosition == .bottom) {
                tabBar
                }
        }
        .padding(0)
        }
    }
    

    我添加了一个屏幕截图,不知道如何使其显示内联。

    modified CustomTabView

    【讨论】:

      【解决方案3】:

      您可以通过应用负填充并使用您自己的控件视图设置可见选项卡项来简单地隐藏 TabView 的边框。

      struct MyView : View
      {
          @State private var selectedTab : Int = 1
      
          var body: some View
          {
              HSplitView
              {
                  Picker("Tab Selection", selection: $selectedTab)
                  {
                      Text("A")
                          .tag(1)
      
                      Text("B")
                          .tag(2)
                  }
      
      
                  TabView(selection: $selectedTab)
                  {
                      ViewA()
                          .tag(1)
      
                      ViewB()
                          .tag(2)
                  }
                  // applying negative padding to hide the ugly frame
                  .padding(EdgeInsets(top: -26.5, leading: -3, bottom: -3, trailing: -3))
              }
          }
      }
      

      当然,只有在 Apple 不对 TabView 的视觉设计进行任何更改的情况下,此 hack 才有效。

      【讨论】:

        猜你喜欢
        • 2016-04-16
        • 1970-01-01
        • 2012-08-10
        • 1970-01-01
        • 2015-12-15
        • 1970-01-01
        • 1970-01-01
        • 2012-06-20
        • 1970-01-01
        相关资源
        最近更新 更多