How to create Tabs with SwiftUI

Last updated on: January 20, 2022

To create Tabs in SwiftUI, we’re going to use a TabView with a PageTabViewStyle, and at the top, we’re going to have a View (Tabs) that is a ScrollView with an array of buttons stacked horizontally (HStack)

Creating the Tabs

First, we’re going to create Tabs in a new SwiftUI View file

Go to your project folder, Right-Click > New File…

Select SwiftUI View, press Next, and name it Tabs.

Inside the file, paste the following code:

struct Tab {
    var icon: Image?
    var title: String
}
struct Tabs: View {
    var fixed = true
    var tabs: [Tab]
    var geoWidth: CGFloat
    @Binding var selectedTab: Int
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            ScrollViewReader { proxy in
                VStack(spacing: 0) {
                    HStack(spacing: 0) {
                        ForEach(0 ..< tabs.count, id: \.self) { row in
                            Button(action: {
                                withAnimation {
                                    selectedTab = row
                                }
                            }, label: {
                                VStack(spacing: 0) {
                                    HStack {
                                        // Image
                                        AnyView(tabs[row].icon)
                                            .foregroundColor(.white)
                                            .padding(EdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 0))
                                        // Text
                                        Text(tabs[row].title)
                                            .font(Font.system(size: 18, weight: .semibold))
                                            .foregroundColor(Color.white)
                                            .padding(EdgeInsets(top: 10, leading: 3, bottom: 10, trailing: 15))
                                    }
                                    .frame(width: fixed ? (geoWidth / CGFloat(tabs.count)) : .none, height: 52)
                                    // Bar Indicator
                                    Rectangle().fill(selectedTab == row ? Color.white : Color.clear)
                                        .frame(height: 3)
                                }.fixedSize()
                            })
                                .accentColor(Color.white)
                                .buttonStyle(PlainButtonStyle())
                        }
                    }
                    .onChange(of: selectedTab) { target in
                        withAnimation {
                            proxy.scrollTo(target)
                        }
                    }
                }
            }
        }
        .frame(height: 55)
        .onAppear(perform: {
            UIScrollView.appearance().backgroundColor = UIColor(#colorLiteral(red: 0.6196078431, green: 0.1098039216, blue: 0.2509803922, alpha: 1))
            UIScrollView.appearance().bounces = fixed ? false : true
        })
        .onDisappear(perform: {
            UIScrollView.appearance().bounces = true
        })
    }
}
struct Tabs_Previews: PreviewProvider {
    static var previews: some View {
        Tabs(fixed: true,
             tabs: [.init(icon: Image(systemName: "star.fill"), title: "Tab 1"),
                    .init(icon: Image(systemName: "star.fill"), title: "Tab 2"),
                    .init(icon: Image(systemName: "star.fill"), title: "Tab 3")],
             geoWidth: 375,
             selectedTab: .constant(0))
    }
}
Code language: Swift (swift)

At the top of the file, we have the model for each Tab, which has an icon (optional) and a title

If you have more than three tabs, set the fixed mode to false so you can scroll through all the tabs.

The ScrollViewReader changes the tabs automatically when you’re swiping between the Views.

Creating the TabView

In our main View (ContentView), create an integer @State variable with the name selectedTab and set an array of tabs.

struct ContentView: View {
    @State private var selectedTab: Int = 0
    let tabs: [Tab] = [
        .init(icon: Image(systemName: "music.note"), title: "Music"),
        .init(icon: Image(systemName: "film.fill"), title: "Movies"),
        .init(icon: Image(systemName: "book.fill"), title: "Books")
    ]
    var body: some View {
        // ...
    }
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}Code language: Swift (swift)

Next, in the body, use the GeometryReader to measure the screen’s width.

In fixed mode, we take the screen width and divide it by the number of tabs to make them with equal widths.

struct ContentView: View {
    // ...
    var body: some View {
        NavigationView {
            GeometryReader { geo in
                // ...
            }
        }
    }
}Code language: Swift (swift)

Next, add in a VStack the Tabs and the TabView with a PageTabViewStyle.

struct ContentView: View {
    // ...
    var body: some View {
        NavigationView {
            // ...
            VStack(spacing: 0) {
                // Tabs
                Tabs(tabs: tabs, geoWidth: geo.size.width, selectedTab: $selectedTab)
                // Views
                TabView(selection: $selectedTab,
                        content: {
                            Demo1View()
                                .tag(0)
                            Demo2View()
                                .tag(1)
                            Demo3View()
                                .tag(2)
                        })
                    .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
            }
        }
    }
}Code language: Swift (swift)

So, at the end (with all the styles, colors, e.t.c), it will look like this:

struct ContentView: View {
    @State private var selectedTab: Int = 0

    let tabs: [Tab] = [
        .init(icon: Image(systemName: "music.note"), title: "Music"),
        .init(icon: Image(systemName: "film.fill"), title: "Movies"),
        .init(icon: Image(systemName: "book.fill"), title: "Books")
    ]

    init() {
        let appearance = UINavigationBarAppearance()
        appearance.configureWithOpaqueBackground()
        appearance.backgroundColor = UIColor(#colorLiteral(red: 0.737254902, green: 0.1294117647, blue: 0.2941176471, alpha: 1))
        UINavigationBar.appearance().standardAppearance = appearance
        UINavigationBar.appearance().scrollEdgeAppearance = appearance
        UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white]
        UINavigationBar.appearance().isTranslucent = false
    }

    var body: some View {
        NavigationView {
            GeometryReader { geo in
                VStack(spacing: 0) {
                    // Tabs
                    Tabs(tabs: tabs, geoWidth: geo.size.width, selectedTab: $selectedTab)

                    // Views
                    TabView(selection: $selectedTab,
                            content: {
                                Demo1View()
                                    .tag(0)
                                Demo2View()
                                    .tag(1)
                                Demo3View()
                                    .tag(2)
                            })
                        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
                }
                .foregroundColor(Color(#colorLiteral(red: 0.737254902, green: 0.1294117647, blue: 0.2941176471, alpha: 1)))
                .navigationBarTitleDisplayMode(.inline)
                .navigationTitle("TabsSwiftUIExample")
                .ignoresSafeArea()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}Code language: Swift (swift)
You can find the final project here

If you have any questionsplease feel free to leave a comment below

Subscribe
Notify of
guest
4 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments