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