Data note
25

Data note · Published Oct 11, 2025 · Updated Aug 28, 2026

What SwiftUI's Tab APIs Actually Do

I rebuilt SwiftUI's TabView examples to separate the iOS 18 navigation APIs from the iOS 26 Liquid Glass redesign—and to see where the system takes control.

A field-guide drawing of a California black bear
In this article7 sections

I originally described SwiftUI’s Tab API as new in iOS 26. That was wrong.

The type-safe Tab syntax, search role, TabSection, and .sidebarAdaptable style arrived with iOS 18. iOS 26 changed how those structures look and behave when an app is built with the newer SDK: the tab bar floats above content, uses Liquid Glass, can minimize while scrolling, and gives a search tab a more distinct transition.

That distinction matters. One release changed how an app describes its navigation; the next changed how the system presents it.

ReleaseWhat changed
iOS 18 / iPadOS 18Type-safe Tab values, search roles, sections, customization, and adaptable tab/sidebar navigation
iOS 26 / iPadOS 26Liquid Glass appearance, tab-bar minimization, bottom accessories, and a search tab that can transform into the search field

I rebuilt the examples in a small Xcode project so I could test the API as a system rather than as isolated snippets.

The Tab Syntax Is About Structure

Before iOS 18, a common SwiftUI tab looked like this:

TabView {
    HomeView()
        .tabItem {
            Label("Home", systemImage: "house")
        }
        .tag(Destination.home)
}

The newer form puts the destination, label, image, and selection value in one declaration:

TabView(selection: $selection) {
    Tab("Home", systemImage: "house", value: .home) {
        HomeView()
    }

    Tab("Settings", systemImage: "gear", value: .settings) {
        SettingsView()
    }
}

This is more than shorter syntax. All selection values in the TabView must have the same type, so mismatches become compiler errors instead of navigation bugs. Apple introduced this form in iOS 18, as shown in its WWDC24 session on tabs and sidebars.

For a tab view with no programmatic routing, the value and selection binding are optional:

TabView {
    Tab("Home", systemImage: "house") {
        HomeView()
    }

    Tab("Profile", systemImage: "person") {
        ProfileView()
    }
}

SwiftUI owns the selected-tab interaction and the platform presentation. It does not persist a selected tab across launches unless the app stores that state itself.

A Search Role Is Not a Search Implementation

This was the most important gap in my first example. Tab(role: .search) tells the system which destination represents search, but the role does not filter any data. The app still needs search state, a .searchable modifier, and content that responds to the query.

struct SearchTabView: View {
    @State private var query = ""

    private let topics = [
        "SwiftUI", "UIKit", "Accessibility", "Performance", "Testing"
    ]

    var body: some View {
        TabView {
            Tab("Articles", systemImage: "newspaper") {
                ArticlesView()
            }

            Tab(role: .search) {
                NavigationStack {
                    List(filteredTopics, id: \.self) { topic in
                        Text(topic)
                    }
                    .navigationTitle("Search")
                }
            }
        }
        .searchable(text: $query, prompt: "Search topics")
    }

    private var filteredTopics: [String] {
        query.isEmpty
            ? topics
            : topics.filter { $0.localizedCaseInsensitiveContains(query) }
    }
}

The role supplies the default search label, symbol, and pinned placement. In iOS 26, selecting that tab can replace the tab bar with the search field. Apple recommends putting .searchable on the TabView when search applies to the tab-based experience; its iOS 26 SwiftUI design walkthrough demonstrates the complete pattern.

Another correction: multiple search roles are not “undefined behavior.” Apple’s current documentation says a searchable tab view prefers the first tab with the search role. That is still a good reason to declare one clear search destination, but it is different from claiming the app will behave unpredictably.

Badges Are Supplementary State

A numeric badge belongs on the Tab, not inside its destination view:

Tab("Messages", systemImage: "message") {
    MessagesView()
}
.badge(unreadCount)

When the integer is zero, the system hides the badge. SwiftUI owns its position, shape, and accessibility presentation. The app owns the count and whether that count is meaningful.

This is a useful boundary: a badge should communicate a small piece of optional information, not become a second label or a permanent decoration.

sidebarAdaptable Does Not Mean “iPad Equals Sidebar”

I had also reduced .sidebarAdaptable to a size-class switch. The real behavior is more interesting.

TabView {
    Tab("Home", systemImage: "house") {
        HomeView()
    }

    TabSection("Library") {
        Tab("Articles", systemImage: "doc.text") {
            ArticlesView()
        }

        Tab("Videos", systemImage: "video") {
            VideosView()
        }
    }
}
.tabViewStyle(.sidebarAdaptable)

On iPhone, this remains a bottom tab bar. On iPad, it starts as a top tab bar that can expand into a sidebar. On macOS and tvOS, the same style presents as a sidebar; visionOS uses an ornament with a sidebar for secondary tabs. Apple’s tab navigation sample documents those platform differences.

TabSection describes hierarchy; it does not guarantee that every destination will fit into compact tab-bar space. For a large information architecture, Apple shows a deliberate compact-width alternative: expose a smaller set of primary tabs and put the deeper destinations behind a Browse tab. That is more predictable than assuming the system will turn an unlimited hierarchy into a good phone UI.

Customization is opt-in as well. A customizable tab needs a stable .customizationID, a TabViewCustomization binding, and storage such as @AppStorage if the choices should survive a relaunch.

What iOS 26 Adds

When built with the iOS 26 SDK, an ordinary system TabView adopts the new appearance without a custom glass background:

  • The tab bar floats above the app’s content.
  • Its Liquid Glass material adapts to what scrolls beneath it.
  • The app can use tabBarMinimizeBehavior(_:) to control supported scroll minimization on iPhone.
  • A tab view can host a bottom accessory for persistent playback or status controls.
  • A search-role tab receives the dedicated search transition described above.

I would not recreate that glass surface. The system already coordinates contrast, motion, safe areas, selection, and platform changes. Custom content still needs its own accessibility labels, Dynamic Type behavior, and sensible navigation state; TabView cannot supply those automatically.

The Practical Boundary

After rebuilding the examples, this is the division I use:

The app should decideThe system should decide
Destinations and their hierarchyTab-bar or sidebar presentation
Selection values and deep-link routingMaterial, spacing, and selected appearance
Search data and filteringSearch tab’s default label and placement
Badge meaning and countBadge rendering
Which destinations deserve compact spaceWhat physically fits at the current size

The new syntax is valuable because it gives SwiftUI better structural information. The price is that a tab is not a pixel-level drawing command. I am comfortable with that trade when the hierarchy is clear and I test compact and regular layouts early.

Run the Examples

The complete Xcode project is in the SwiftUI tab and toolbar lab. It includes basic tabs, programmatic selection, a working search destination, badges, paging, and sidebar adaptation.


Related posts:

One quick signal

Did this earn your time?

What was missing?

Thanks. That gives me something concrete to check.