WWDC Quick Look 💓 By SwiftGGTeam
Elevate your tab and sidebar experience in iPadOS

Elevate your tab and sidebar experience in iPadOS

Watch original video

Highlight

iPadOS 18 moves the Tab Bar to the top of the screen, sharing space with the navigation bar. The more compact design frees up room for content, and existing apps built with the iPadOS 18 SDK get the new look without code changes.


Core Content

The Tab Bar on iPad has existed since the beginning of iOS, but iPadOS 18 brings its first major redesign. The old Tab Bar sat at the bottom of the screen, consuming vertical space; the new design moves it to the top, sharing the same safe area as the navigation bar. This not only gives content more room, but also places tab controls closer to other navigation elements like the back button and title, making them easier to reach.

Another long-standing pain point was how Sidebars were built. In iPadOS 17 and earlier, developers had to use UISplitViewController with an Outline View to implement a Sidebar. That approach was cumbersome and unintuitive. iPadOS 18 offers a more direct solution: the same tab hierarchy description can be presented as either a Tab Bar or a Sidebar. When the user taps the expand button, the Tab Bar smoothly transitions into a Sidebar showing more hierarchical content (such as folders and playlists); collapsing it returns to the compact Tab Bar. Both forms share the same state data and switch seamlessly.

Personalization is another focus of this new navigation system. Users can drag tabs from the Sidebar to the Tab Bar to pin them, and hide tabs they do not need. The Tab Bar is divided into three zones: a fixed zone (not customizable), a customizable zone (drag to reorder), and a pinned zone (always visible at the end, such as Search). These customization behaviors are automatically persisted by the system and restored the next time the app opens.

To support these features, SwiftUI introduces Tab and TabSection syntax, and UIKit introduces UITab and UITabGroup. Both are used to describe the app’s navigation hierarchy in a unified way, rather than maintaining two separate navigation implementations.


Detailed Content

SwiftUI updated the TabView syntax in iOS 18. Instead of the old tag/selection pattern, you declare Tab structs directly. Each Tab includes a title, icon, and content view, with an optional selection value for programmatic control. The new syntax checks type consistency at compile time, avoiding runtime errors (04:27).

TabView {
    Tab("Watch Now", systemImage: "play") {
        WatchNowView()
    }
    Tab("Library", systemImage: "books.vertical") {
        LibraryView()
    }
    // ...
}

UIKit provides corresponding APIs. Create UITab instances to describe each top-level section, then assign them to tabBarController.tabs. Changes to UITab instances are reflected immediately in the UI (04:58).

tabBarController.tabs = [
    UITab(title: "Watch Now", image: UIImage(systemName: "play"), identifier: "Tabs.watchNow") { _ in
       WatchNowViewController()
    },
    UITab(title: "Library", image: UIImage(systemName: "books.vertical"), identifier: "Tabs.library") { _ in
       LibraryViewController()
    },
    // ...
]

Key points:

  • UITab is a lightweight description structure, not a view controller
  • identifier tracks the tab’s unique identity, related to customization features
  • The _ parameter in the closure represents the UITab itself, available for later reference

Search is common across apps. SwiftUI provides Tab(role: .search) and UIKit provides UISearchTab. Using these dedicated APIs, the system automatically configures the default title, magnifying glass icon, and pins the search tab at the end of the Tab Bar (05:58).

// SwiftUI
Tab(role: .search) {
    SearchView()
}

// UIKit
let searchTab = UISearchTab {
    SearchViewController()
}

Key points:

  • The search tab automatically gets a fixed position (pinned placement)
  • No need to manually set icon and title — the system provides defaults
  • Also appears in the Sidebar, following normal sort order

To enable the Sidebar, set tabViewStyle to .sidebarAdaptable in SwiftUI. Then use TabSection to group related tabs. In the Sidebar, TabSection appears as an expandable/collapsible group; in the Tab Bar, tabs within a group are displayed flat (06:41).

TabView {
    Tab("Watch Now", systemImage: "play") {
        // ...
    }
    Tab("Library", systemImage: "books.vertical") {
        // ...
    }
    // ...
    TabSection("Collections") {
        Tab("Cinematic Shots", systemImage: "list.and.film") {
            // ...
        }
        Tab("Forest Life", systemImage: "list.and.film") {
            // ...
        }
        // ...
    }
    TabSection("Animations") {
        // ...
    }
    Tab(role: .search) {
        // ...
    }
}
.tabViewStyle(.sidebarAdaptable)

Key points:

  • TabSection displays as a group header in the Sidebar
  • Tabs are arranged in declaration order; Sections come after individual tabs
  • The search tab’s position is unaffected by Sections — the system determines its fixed position

To enable the Sidebar in UIKit, set tabBarController.mode to .tabSidebar. Use UITabGroup for grouping; its children property can be updated dynamically (07:16).

let collectionsGroup = UITabGroup(
    title: "Collections",
    image: UIImage(systemName: "folder"),
    identifier: "Tabs.CollectionsGroup"
    children: self.collectionsTabs()) { _ in
        // ...
}

tabBarController.mode = .tabSidebar
tabBarController.tabs = [
    UITab(title: "Watch Now", ...) { _ in
        // ...
    },
    UITab(title: "Library", ...) { _ in
        // ...
    },
    // ...
    collectionsGroup,
    UITabGroup(title: "Animations", ...) { _ in
        // ...
    },
    UISearchTab { _ in
        // ...
    },
]

Key points:

  • UITabGroup is a mutable object; changing children updates the UI immediately
  • identifier tracks group identity, supporting customization features
  • The mode property determines display mode and can be switched dynamically

The Sidebar also supports action buttons for groups, such as “New Station” in a podcast app. Use sectionActions in SwiftUI and sidebarActions in UIKit (07:45).

TabSection(...) {
    // ...
}
.sectionActions {
    Button("New Station", ...) {
        // action
    }
}

// UIKit
let tabGroup = UITabGroup(...)
tabGroup.sidebarActions = [
    UIAction(title: "New Station", ...) { _ in
        // action
    },
]

Key points:

  • Action buttons appear to the right of the group header
  • Clicking an action does not change the currently selected tab
  • Different groups can have different actions

Tabs can also serve as drop destinations. In a Photos app, users can drag photos onto a collection tab to add them directly. Use the dropDestination modifier in SwiftUI, and implement two methods from UITabBarControllerDelegate in UIKit (08:12).

// SwiftUI
Tab(collection.name, image: collection.image) {
    CollectionDetailView(collection)
}
.dropDestination(for: Photo.self) { photos in
    // Add 'photos' to the specified collection
}

// UIKit
func tabBarController(
    _ tabBarController: UITabBarController,
    tab: UITab, operationForAcceptingItemsFrom dropSession: any UIDropSession
) -> UIDropOperation {
    session.canLoadObjects(ofClass: Photo.self) ? .copy : .cancel
}

func tabBarController(
    _ tabBarController: UITabBarController,
    tab: UITab, acceptItemsFrom dropSession: any UIDropSession) {
    session.loadObjects(ofClass: Photo.self) { photos in
        // Add 'photos' to the specified collection
    }
}

Key points:

  • The first method determines whether drop content can be accepted
  • The second method receives the actual data objects
  • Drop operations work in both Tab Bar and Sidebar

User customization is a highlight of this update. To enable customization in SwiftUI, attach a TabViewCustomization to TabView and persist state with @AppStorage (10:45).

@AppStorage("MyTabViewCustomization")
private var customization: TabViewCustomization

TabView {
    Tab("Watch Now", systemImage: "play", value: .watchNow) {
        // ...
    }
    .customizationID("Tab.watchNow")
    // ...
    TabSection("Collections") {
        ForEach(MyCollectionsTab.allCases) { tab in
            Tab(...) {
                // ...
            }
            .customizationID(tab.customizationID)
        }
    }
    .customizationID("Tab.collections")
    // ...
}
.tabViewCustomization($customization)

Key points:

  • customizationID is the unique identifier for a tab participating in customization
  • @AppStorage ensures customization state persists across app restarts
  • The value parameter enables programmatic selection of a tab

Some tabs should not be customizable, such as core feature entry points. Use customizationBehavior to disable customization (11:40).

Tab("Watch Now", systemImage: "play", value: .watchNow) {
    // ...
}
.customizationBehavior(.disabled, for: .sidebar, .tabBar)

Tab("Optional Tab", ...) {
    // ...
}
.customizationID("Tab.example.optional")
.defaultVisibility(.hidden, for: .tabBar)

Key points:

  • .disabled means the tab cannot be customized in sidebar/tabBar
  • .defaultVisibility(.hidden) hides the tab by default but allows users to add it
  • Tabs that do not participate in customization do not need a customizationID

To enable customization in UIKit, set allowsHiding and preferredPlacement (12:38).

let myTab = UITab(...)
myTab.allowsHiding = true
print(myTab.isHidden)

// .default, .optional, .movable, .pinned, .fixed, .sidebarOnly
myTab.preferredPlacement = .fixed

let myTabGroup = UITabGroup(...)
myTabGroup.allowsReordering = true
myTabGroup.displayOrderIdentifiers = [...]

Key points:

  • allowsHiding controls whether the user can hide the tab
  • preferredPlacement determines the tab’s default position and behavior
  • displayOrderIdentifiers records the user-customized order

UIKit notifies customization changes through delegate methods (12:39).

func tabBarController(_ tabBarController: UITabBarController, visibilityDidChangeFor tabs: [UITab]) {
    // Read 'tab.isHidden' for the updated visibility.
}

func tabBarController(_ tabBarController: UITabBarController, displayOrderDidChangeFor group: UITabGroup) {
    // Read 'group.displayOrderIdentifiers' for the updated order.
}

Key points:

  • The first method is called when tab visibility changes
  • The second method is called when a group’s display order changes
  • Use these methods to sync to other UI or data models

Regarding icon styles: the Tab Bar prefers filled glyphs, while the Sidebar prefers outline styles. When using SF Symbols, provide only the outline variant — the system automatically switches to the filled variant in the Tab Bar (05:25). This behavior is consistent across iPad, iPhone, and visionOS.


Core Takeaways

  1. Verify appearance changes by building with the new SDK

    • Why it’s worth doing: iPadOS 18 Tab Bar appearance changes take effect without code changes — building first is the fastest way to verify
    • How to start: Set the project’s deployment target to iPadOS 18, build and run with Xcode 16, and observe the automatic Tab Bar upgrade
  2. Use SF Symbols outline variants

    • Why it’s worth doing: The system automatically handles icon differences between Tab Bar and Sidebar — no need to prepare two sets of images
    • How to start: Use the systemName parameter in Tab or UITab, choosing outline versions of symbols (such as square.grid.2x2)
  3. Use dedicated APIs for search

    • Why it’s worth doing: Tab(role: .search) and UISearchTab automatically get a fixed position and default icon, matching user expectations for search placement
    • How to start: Replace existing search tabs with these dedicated APIs and remove manual icon and title settings
  4. Control tab count and use Sidebar for more content

    • Why it’s worth doing: Too many tabs in the Tab Bar makes them hard to find; the Sidebar can hold more hierarchy without feeling crowded
    • How to start: Move secondary, categorized content into TabSection or UITabGroup, keeping the Tab Bar lean
  5. Disable customization for important tabs

    • Why it’s worth doing: Core feature entry points should always be visible, preventing users from accidentally hiding them
    • How to start: Use customizationBehavior(.disabled) or preferredPlacement = .fixed to protect key tabs

Comments

GitHub Issues · utterances