TabbedView in SwiftUI

How to quickly implement and style a tabbed UI using SwiftUI.

June 28, 2019

What is a TabbedView?

A TabbedView is SwiftUI’s version of the UITabBar from UIKit. This control lets the user quickly switch between different sections of your app.

A basic example of a TabbedView.

When deciding what control to use, I would recommend taking a look at the iOS Human Interface Guidelines. The section on tab bars can be found here.


Adding a basic TabbedView

Xcode will generate the following view when dragging a TabbedView into your view:

TabbedView(selection: .constant(1)) {
    Text("Tab 1!").tabItemLabel(Text("Tab 1")).tag(1)
    Text("Tab 2!").tabItemLabel(Text("Tab 2")).tag(2)
}

Note: In Xcode 11 beta 2 the tab items aren’t listed in the preview pane. You will need to build and run the app to see the tabs.

This looks super simple, but there’s more work needed. If you build and run the app and tap “Tab 2”, you’ll notice the text on the screen doesn’t change. We would expect it to change to “Tab 2!”, but nothing happens. To allow changing the tab, we need to add a state variable for the selection, like so:

struct ContentView : View {
    @State private var selection = 1
    
    var body: some View {
        TabbedView(selection: $selection) {
            Text("Tab 1!").tabItemLabel(Text("Tab 1")).tag(1)
            Text("Tab 2!").tabItemLabel(Text("Tab 2")).tag(2)
        }
    }
}

Note we’re using the $ symbol to bind the selection to the private variable.

Now, you’ll be able to change the tab and see the content change.


Styling the TabbedView

Styling the TabbedView by adding images is simple in SwiftUI. Unfortunately, beta 2 has a bug preventing us from writing the following:

struct ContentView : View {
    @State private var selection = 1
    
    var body: some View {
        TabbedView(selection: $selection) {
            Text("Tab 1!").tabItemLabel(
              Image("book")
              Text("Tab 1")
            ).tag(1)
            Text("Tab 2!").tabItemLabel(
              Image("book")
              Text("Tab 2")
            ).tag(2)
        }
    }
}

Instead, we need to add a VStack, which produces the following code:

struct ContentView : View {
    @State private var selection = 1
    
    var body: some View {
        TabbedView(selection: $selection) {
            Text("Tab 1!").tabItemLabel(
                VStack {
                    Image("book")
                    Text("Tab 1")
            }).tag(1)
            Text("Tab 2!").tabItemLabel(
                VStack {
                    Image("book")
                    Text("Tab 2")
            }).tag(2)
        }
    }
}

This is because the tabItemLabel(_:) modifier doesn’t accept @ViewBuilder closures according to Apple’s beta 2 release notes.

Also note that you can’t use SF Glyphs in a TabbedView as of beta 2.


Troubleshooting

Question: Why is my tab bar image a solid color box?

Images in the tabbed view need to have an opacity layer. An icon with a white background (for example) will appear as a solid box.


Question: I am getting the following error: Cannot convert value of type ‘Int’ to expected argument type ‘Binding<_>'

This is because you need to pass a Binding for the selection, and not an Int.

Correct: TabbedView(selection: $selection)
Incorrect: TabbedView(selection: selection)


Question: Why are SF Glyphs not working when I use Image(systemName: "book") inside my TabbedView?

As of Xcode 11 beta 2, SF Glyphs do not seem to be working in TabbedView.


Question: Why do the tabs not appear in the SwiftUI preview pane?

This seems to be a bug in Xcode 11 beta 2.


Question: I am getting the following error: Expected ',' separator

If your TabbedView code looks like this:

TabbedView(selection: $selection) {
        Text("Tab 1!").tabItemLabel(
            Image("book")
            Text("Tab 1!")).tag(1)
        Text("Tab 2!").tabItemLabel(
            Image("book")
            Text("Tab 1!")).tag(2)
    }
}

Then the error is occurring because TabbedViews in Xcode 11 beta 2 do not support @ViewBuilder closures. You will need to wrap the Image and Text in a VStack, as shown above.