skip to Main Content

I’m trying to replicate the tab bar in the iPad version of Safari, which looks like this:

enter image description here

(Is there a third party library which does this? I can’t find one)

I’m using the code below. Which results in this:

enter image description here

I guess I need to turn the Views into arrays somehow, and have a button (on the tab, or page) to add and remove tabs. Any idea how to do this?

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

2

Answers


  1. Like a lot of problems with SwiftUI, this seems overly complex because you’re mixing the concept of state with how that state is drawn on screen.

    Removing the visuals for a moment, what you have in terms of data is:

    • an ordered collection of pages, each with a title and image
    • a concept of which page in that collection is active

    (Note that I call them ‘pages’ here to try and separate them from the visual representation as tabs.)

    You also have some actions which will alter that data:

    • your user can choose which page should be the active one
    • they can add a new page to the collection
    • they can remove an existing page from the collection

    Now, if you think of those small steps as your data model, you can build an object or objects which cleanly encapsulate that.

    Then, you can go on to determine how SwiftUI represents that data and how it might include the action triggers. For example:

    • A list of horizontal tabs loops through the ordered collection of pages and renders each one
    • Each tab has a button action which, when tapped, sets that button to be the active one
    • Each tab has a close button which, when tapped, removes it from the pages collection
    • A separate button, when tapped, can add a new page to the collection

    And so on. Hopefully, you can see that each view in SwiftUI would now have a specific purpose, and should be easier for you to think about.

    And you might even decide on a different UI – you could list your pages vertically in a List, for example, or in a grid like the iPhone’s Safari page view. But even if you did, your underlying data wouldn’t change.

    Login or Signup to reply.
  2. I honestly love @Scott Matthewman’s answer! It inspired me to try an implementation – I included Scotts to do points as remarks 🙂

    Model:

    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: {$0 == page})
        }
    }
    

    Views:

    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()
                }
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search