SwiftUI 中具有可变数量标签的自定义标签栏
Custom Tab Bar with variable number of tabs in SwiftUI
我正在尝试复制 iPad 版本的 Safari 中的标签栏,如下所示:
(是否有第三方库可以执行此操作?我找不到)
我正在使用下面的代码。结果是:
我想我需要以某种方式将视图转换为数组,并有一个按钮(在选项卡或页面上)来添加和删除选项卡。知道怎么做吗?
import SwiftUI
struct TabLabel : View {
var text : String
var imageName : String
var color : Color
var body : some View {
VStack() {
Image(systemName: imageName)
Text(text).font(.caption)
}.foregroundColor(color)
}
}
struct TabButton : View {
@Binding var currentSelection : Int
var selectionIndex : Int
var label : TabLabel
var body : some View {
Button(action: { self.currentSelection = self.selectionIndex }) { label }.opacity(selectionIndex == currentSelection ? 0.5 : 1.0)
}
}
struct CustomTabBarView<SomeView1 : View, SomeView2 : View, SomeView3 : View> : View {
var view1 : SomeView1
var view2 : SomeView2
var view3 : SomeView3
@State var currentSelection : Int = 1
var body : some View {
let label1 = TabLabel(text: "First", imageName: "1.square.fill", color: Color.red)
let label2 = TabLabel(text: "Second", imageName: "2.square.fill", color: Color.purple)
let label3 = TabLabel(text: "Third", imageName: "3.square.fill", color: Color.blue)
let button1 = TabButton(currentSelection: $currentSelection, selectionIndex: 1, label: label1)
let button2 = TabButton(currentSelection: $currentSelection, selectionIndex: 2, label: label2)
let button3 = TabButton(currentSelection: $currentSelection, selectionIndex: 3, label: label3)
return VStack() {
HStack() {
button1
Spacer()
button2
Spacer()
button3
}.padding(.horizontal, 48)
.frame(height: 48.0)
.background(Color(UIColor.systemGroupedBackground))
Spacer()
if currentSelection == 1 {
view1
}
else if currentSelection == 2 {
view2
}
else if currentSelection == 3 {
view3
}
Spacer()
}
}
}
struct ContentView: View {
@State private var showGreeting = true
var body: some View {
let view1 = VStack() {
Text("The First Tab").font(.headline)
Image(systemName: "triangle").resizable().aspectRatio(contentMode: .fit).frame(width: 100)
}
let view2 = Text("Another Tab").font(.headline)
let view3 = Text("The Final Tab").font(.headline)
return CustomTabBarView(view1: view1, view2: view2, view3: view3)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
与 Swift 的许多问题一样UI,这似乎过于复杂,因为您将 state 的概念与该状态在屏幕上的绘制方式混合在一起。
暂时删除视觉效果,您拥有的数据是:
- 有序的 collection 页面,每个页面都有标题和图片
- collection 中哪个页面处于活动状态的概念
(请注意,我在这里称它们为 'pages' 以尝试将它们作为选项卡从视觉表示中分离出来。)
您还有一些操作会改变该数据:
- 您的用户可以选择哪个页面应该是活动页面
- 他们可以将新页面添加到 collection
- 他们可以从 collection
中删除现有页面
现在,如果您将这些小步骤视为您的数据模型,则可以构建一个 object 或 object 来完全封装它。
然后,您可以继续确定 SwiftUI 如何表示该数据以及它可能如何包含操作触发器。例如:
- 水平选项卡列表循环遍历有序的 collection 页面并呈现每个页面
- 每个选项卡都有一个按钮操作,当点击该按钮时,将该按钮设置为活动按钮
- 每个选项卡都有一个关闭按钮,点击该按钮会将其从页面中删除 collection
- 一个单独的按钮,当点击时,可以向 collection
添加一个新页面
等等。希望您能看到 SwiftUI 中的每个视图现在都有特定的用途,并且应该更容易让您考虑。
您甚至可以决定使用不同的 UI – 例如,您可以在 List
中垂直列出您的页面,或者像 iPhone 的 Safari 一样在网格中页面预览。但即使您这样做了,您的基础数据也不会改变。
老实说,我喜欢@Scott Matthewman 的回答!它激发了我尝试实现的灵感——我包括 Scotts 做点作为评论:)
型号:
struct SinglePage: Identifiable, Equatable {
var id: UUID
var title: String
var image: String
init(title: String, image: String) {
self.id = UUID()
self.title = title
self.image = image
}
static func == (lhs: SinglePage, rhs: SinglePage) -> Bool {
return lhs.id == rhs.id
}
}
class PagesModel: ObservableObject {
// an ordered collection of pages, each with a title and image
@Published var pages: [SinglePage]
// a concept of which page in that collection is active
@Published var selectedPage: SinglePage?
init() {
// Test Data
pages = []
for i in 0..<4 {
let item = SinglePage(title: "Tab \(i)", image: "\(i).circle")
self.pages.append(item)
}
selectedPage = pages.first ?? nil
}
// your user can choose which page should be the active one
func select(page: SinglePage) {
selectedPage = page
}
// they can add a new page to the collection
func add(title: String, image: String) {
let item = SinglePage(title: title, image: image)
self.pages.append(item)
}
// they can remove an existing page from the collection
func delete(page: SinglePage) {
pages.removeAll(where: {[=10=] == page})
}
}
观看次数:
struct ContentView: View {
@StateObject var tabs = PagesModel()
var body: some View {
VStack {
// A list of horizontal tabs loops through the ordered collection of pages and renders each one
HStack {
ForEach(tabs.pages) { page in
TabLabelView(page: page)
}
// A separate button, when tapped, can add a new page to the collection
AddTabButton()
}
ActiveTabContentView(page: tabs.selectedPage)
}
.environmentObject(tabs)
}
}
struct TabLabelView: View {
@EnvironmentObject var tabs: PagesModel
let page: SinglePage
var body: some View {
HStack {
// Each tab has a close button which, when tapped, removes it from the pages collection
Button {
tabs.delete(page: page)
} label: {
Image(systemName: "xmark")
}
Text(page.title)
}
.font(.caption)
.padding(5)
// .frame(height: 50)
.background(
Color(page == tabs.selectedPage ? .red : .gray)
)
// Each tab has a button action which, when tapped, sets that button to be the active one
.onTapGesture {
tabs.select(page: page)
}
}
}
// A separate button, when tapped, can add a new page to the collection
struct AddTabButton: View {
@EnvironmentObject var tabs: PagesModel
var body: some View {
Button {
tabs.add(title: "New", image: "star")
} label: {
Label("Add", systemImage: "add")
}
.font(.caption)
.padding(5)
}
}
struct ActiveTabContentView: View {
@EnvironmentObject var tabs: PagesModel
let page: SinglePage?
var body: some View {
if let page = page {
VStack {
Spacer()
Text(page.title)
Image(systemName: page.image)
.font(.largeTitle)
Spacer()
}
}
}
}
我正在尝试复制 iPad 版本的 Safari 中的标签栏,如下所示:
(是否有第三方库可以执行此操作?我找不到)
我正在使用下面的代码。结果是:
我想我需要以某种方式将视图转换为数组,并有一个按钮(在选项卡或页面上)来添加和删除选项卡。知道怎么做吗?
import SwiftUI
struct TabLabel : View {
var text : String
var imageName : String
var color : Color
var body : some View {
VStack() {
Image(systemName: imageName)
Text(text).font(.caption)
}.foregroundColor(color)
}
}
struct TabButton : View {
@Binding var currentSelection : Int
var selectionIndex : Int
var label : TabLabel
var body : some View {
Button(action: { self.currentSelection = self.selectionIndex }) { label }.opacity(selectionIndex == currentSelection ? 0.5 : 1.0)
}
}
struct CustomTabBarView<SomeView1 : View, SomeView2 : View, SomeView3 : View> : View {
var view1 : SomeView1
var view2 : SomeView2
var view3 : SomeView3
@State var currentSelection : Int = 1
var body : some View {
let label1 = TabLabel(text: "First", imageName: "1.square.fill", color: Color.red)
let label2 = TabLabel(text: "Second", imageName: "2.square.fill", color: Color.purple)
let label3 = TabLabel(text: "Third", imageName: "3.square.fill", color: Color.blue)
let button1 = TabButton(currentSelection: $currentSelection, selectionIndex: 1, label: label1)
let button2 = TabButton(currentSelection: $currentSelection, selectionIndex: 2, label: label2)
let button3 = TabButton(currentSelection: $currentSelection, selectionIndex: 3, label: label3)
return VStack() {
HStack() {
button1
Spacer()
button2
Spacer()
button3
}.padding(.horizontal, 48)
.frame(height: 48.0)
.background(Color(UIColor.systemGroupedBackground))
Spacer()
if currentSelection == 1 {
view1
}
else if currentSelection == 2 {
view2
}
else if currentSelection == 3 {
view3
}
Spacer()
}
}
}
struct ContentView: View {
@State private var showGreeting = true
var body: some View {
let view1 = VStack() {
Text("The First Tab").font(.headline)
Image(systemName: "triangle").resizable().aspectRatio(contentMode: .fit).frame(width: 100)
}
let view2 = Text("Another Tab").font(.headline)
let view3 = Text("The Final Tab").font(.headline)
return CustomTabBarView(view1: view1, view2: view2, view3: view3)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
与 Swift 的许多问题一样UI,这似乎过于复杂,因为您将 state 的概念与该状态在屏幕上的绘制方式混合在一起。
暂时删除视觉效果,您拥有的数据是:
- 有序的 collection 页面,每个页面都有标题和图片
- collection 中哪个页面处于活动状态的概念
(请注意,我在这里称它们为 'pages' 以尝试将它们作为选项卡从视觉表示中分离出来。)
您还有一些操作会改变该数据:
- 您的用户可以选择哪个页面应该是活动页面
- 他们可以将新页面添加到 collection
- 他们可以从 collection 中删除现有页面
现在,如果您将这些小步骤视为您的数据模型,则可以构建一个 object 或 object 来完全封装它。
然后,您可以继续确定 SwiftUI 如何表示该数据以及它可能如何包含操作触发器。例如:
- 水平选项卡列表循环遍历有序的 collection 页面并呈现每个页面
- 每个选项卡都有一个按钮操作,当点击该按钮时,将该按钮设置为活动按钮
- 每个选项卡都有一个关闭按钮,点击该按钮会将其从页面中删除 collection
- 一个单独的按钮,当点击时,可以向 collection 添加一个新页面
等等。希望您能看到 SwiftUI 中的每个视图现在都有特定的用途,并且应该更容易让您考虑。
您甚至可以决定使用不同的 UI – 例如,您可以在 List
中垂直列出您的页面,或者像 iPhone 的 Safari 一样在网格中页面预览。但即使您这样做了,您的基础数据也不会改变。
老实说,我喜欢@Scott Matthewman 的回答!它激发了我尝试实现的灵感——我包括 Scotts 做点作为评论:)
型号:
struct SinglePage: Identifiable, Equatable {
var id: UUID
var title: String
var image: String
init(title: String, image: String) {
self.id = UUID()
self.title = title
self.image = image
}
static func == (lhs: SinglePage, rhs: SinglePage) -> Bool {
return lhs.id == rhs.id
}
}
class PagesModel: ObservableObject {
// an ordered collection of pages, each with a title and image
@Published var pages: [SinglePage]
// a concept of which page in that collection is active
@Published var selectedPage: SinglePage?
init() {
// Test Data
pages = []
for i in 0..<4 {
let item = SinglePage(title: "Tab \(i)", image: "\(i).circle")
self.pages.append(item)
}
selectedPage = pages.first ?? nil
}
// your user can choose which page should be the active one
func select(page: SinglePage) {
selectedPage = page
}
// they can add a new page to the collection
func add(title: String, image: String) {
let item = SinglePage(title: title, image: image)
self.pages.append(item)
}
// they can remove an existing page from the collection
func delete(page: SinglePage) {
pages.removeAll(where: {[=10=] == page})
}
}
观看次数:
struct ContentView: View {
@StateObject var tabs = PagesModel()
var body: some View {
VStack {
// A list of horizontal tabs loops through the ordered collection of pages and renders each one
HStack {
ForEach(tabs.pages) { page in
TabLabelView(page: page)
}
// A separate button, when tapped, can add a new page to the collection
AddTabButton()
}
ActiveTabContentView(page: tabs.selectedPage)
}
.environmentObject(tabs)
}
}
struct TabLabelView: View {
@EnvironmentObject var tabs: PagesModel
let page: SinglePage
var body: some View {
HStack {
// Each tab has a close button which, when tapped, removes it from the pages collection
Button {
tabs.delete(page: page)
} label: {
Image(systemName: "xmark")
}
Text(page.title)
}
.font(.caption)
.padding(5)
// .frame(height: 50)
.background(
Color(page == tabs.selectedPage ? .red : .gray)
)
// Each tab has a button action which, when tapped, sets that button to be the active one
.onTapGesture {
tabs.select(page: page)
}
}
}
// A separate button, when tapped, can add a new page to the collection
struct AddTabButton: View {
@EnvironmentObject var tabs: PagesModel
var body: some View {
Button {
tabs.add(title: "New", image: "star")
} label: {
Label("Add", systemImage: "add")
}
.font(.caption)
.padding(5)
}
}
struct ActiveTabContentView: View {
@EnvironmentObject var tabs: PagesModel
let page: SinglePage?
var body: some View {
if let page = page {
VStack {
Spacer()
Text(page.title)
Image(systemName: page.image)
.font(.largeTitle)
Spacer()
}
}
}
}