SwiftUI:适用于 macOS 和 iOS 的自定义选项卡视图
SwiftUI: Custom Tab View for macOS & iOS
有没有一种简单的方法可以使用 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)
}
}
}
为了解决这个问题,我整理了以下简单的自定义视图,它提供了与 iOS 更相似的选项卡界面,即使 运行 在 Mac 上也是如此。它仅通过获取一组元组来工作,每个元组概述选项卡的标题、图标名称和内容。
它可以在明暗模式下工作,并且可以在 macOS 或 iOS / iPadOS 等系统上 运行,但你可能运行ning on iOS 时只想使用标准 TabView
实现;由你决定。
它还包含一个参数,因此您可以根据偏好将栏放在顶部或底部(顶部更符合 macOS 准则)。
这是结果示例(在深色模式下):
这是代码。一些注意事项:
- 它使用
Color
的基本扩展,因此它可以使用系统背景颜色,而不是 hard-coding。
- 唯一有点棘手的部分是额外的
background
和 shadow
修饰符,需要它们来防止 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{ [=10=].tabText }
self.tabIconNames = content.map{ [=10=].tabIconName }
self.tabViews = content.map{ [=10=].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)
)
)
]
)
}
}
您可以通过应用负填充并使用您自己的控件视图来设置可见选项卡项来简单地隐藏 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 才会起作用。
回复 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{ [=10=].tabText }
self.tabIconNames = content.map{ [=10=].tabIconName }
self.tabViews = content.map{ [=10=].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
有没有一种简单的方法可以使用 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)
}
}
}
为了解决这个问题,我整理了以下简单的自定义视图,它提供了与 iOS 更相似的选项卡界面,即使 运行 在 Mac 上也是如此。它仅通过获取一组元组来工作,每个元组概述选项卡的标题、图标名称和内容。
它可以在明暗模式下工作,并且可以在 macOS 或 iOS / iPadOS 等系统上 运行,但你可能运行ning on iOS 时只想使用标准 TabView
实现;由你决定。
它还包含一个参数,因此您可以根据偏好将栏放在顶部或底部(顶部更符合 macOS 准则)。
这是结果示例(在深色模式下):
这是代码。一些注意事项:
- 它使用
Color
的基本扩展,因此它可以使用系统背景颜色,而不是 hard-coding。 - 唯一有点棘手的部分是额外的
background
和shadow
修饰符,需要它们来防止 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{ [=10=].tabText }
self.tabIconNames = content.map{ [=10=].tabIconName }
self.tabViews = content.map{ [=10=].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)
)
)
]
)
}
}
您可以通过应用负填充并使用您自己的控件视图来设置可见选项卡项来简单地隐藏 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 才会起作用。
回复 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{ [=10=].tabText }
self.tabIconNames = content.map{ [=10=].tabIconName }
self.tabViews = content.map{ [=10=].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