TabView у SwiftUI для iOS 26: Tab API, sidebarAdaptable і адаптивна навігація

Практичний посібник з нового TabView у SwiftUI для iOS 26: новий тип Tab, стиль sidebarAdaptable, TabSection, роль пошуку, кастомізація й адаптація до iPad, Mac Catalyst і visionOS з прикладами коду.

TabView SwiftUI iOS 26: Tab API та Sidebar

Оновлено: 29 липня 2026 р.

TabView у SwiftUI для iOS 26 — це компонент навігації верхнього рівня, який показує окремі розділи застосунку через вкладки внизу на iPhone і автоматично перетворюється на бокову панель на iPad, Mac Catalyst та visionOS, коли ви застосовуєте стиль .sidebarAdaptable. Замість застарілого ініціалізатора з ярликом і .tag(...) Apple рекомендує новий тип Tab, який підтримує групування через TabSection, кастомізацію користувачем, TabRole.search і візуальну інтеграцію з Liquid Glass. У цьому посібнику я покажу міграцію з нуля, кросплатформенні нюанси й типові пастки, на які натикався у власних проєктах (декілька з них коштували мені по пів дня).

  • Новий тип Tab замінює ініціалізатор TabView { ... }.tabItem { ... }.tag(...) і працює як для статичних, так і для динамічних наборів вкладок.
  • Стиль .tabViewStyle(.sidebarAdaptable) перемикає TabView між нижньою панеллю (iPhone, компактний клас розміру) та бічною панеллю (iPad, macOS, visionOS) без окремого коду.
  • Групи TabSection з’являються як розділи бокової панелі на iPad і Mac, а на iPhone згортаються у плоский список вкладок.
  • TabRole.search закріплює вкладку пошуку у правильному місці інтерфейсу на кожній платформі й додає системну поведінку жестів.
  • Кастомізацію вкладок (перетягування, приховування) вмикає модифікатор .tabViewCustomization(...), який зберігає порядок у AppStorage.
  • З iOS 26 TabView автоматично отримує ефекти Liquid Glass. Приберіть власні напівпрозорі фони, які раніше маскували системну панель.

Що нового в TabView для iOS 26

Головна зміна така: TabView перестав бути «просто панеллю знизу» і став адаптивним контейнером навігації верхнього рівня, який Apple вважає рекомендованим для більшості застосунків, що не є документоорієнтованими. Найважливіші оновлення, які накопичилися між iOS 18 і iOS 26:

  • Новий тип Tab (введений в iOS 18) став обов’язковим у нових API. Старий синтаксис view.tabItem { ... }.tag(id) формально ще працює, але не підтримує групи, кастомізацію та роль пошуку.
  • Стиль .sidebarAdaptable перетворює нижню панель на бокову там, де це має сенс: на iPad у landscape, на Mac Catalyst і в visionOS. Раніше для цього доводилося тримати два дерева view.
  • TabSection дозволяє групувати вкладки в бічній панелі. Це фактично аналог «розділів» у Files або Music.app.
  • Роль пошуку (Tab(role: .search) { ... }) з iOS 18 отримала стабілізовану поведінку в iOS 26: на iPhone це «пошукова» вкладка з системним оформленням, на iPad вона переїжджає в бокову панель поруч із .searchable.
  • Кастомізація користувачем через TabViewCustomization. Те, чого від iPad-навігації хотіли з часів iOS 13.
  • Liquid Glass. TabView у iOS 26 автоматично використовує прозоре скло замість напівпрозорого блюру. Це впливає на кольори іконок і на те, як позначаються обрані вкладки.

Практичний наслідок: якщо ваш проєкт було створено ще під iOS 15–17, вам майже гарантовано доведеться переписати код TabView, щоб не втратити функціональність, яку користувачі очікують на iPad і Mac. Не поспішайте додавати умовний #if os(iOS) навколо старого коду. Новий API покриває всі платформи однією гілкою.

Як створити TabView з новим Tab API

Найпростіший приклад: три статичні вкладки. Порівняйте старий і новий синтаксис поруч:

// ❌ Застарілий стиль (iOS 15–17): працює, але не підтримує групи й кастомізацію
TabView(selection: $selectedTab) {
    HomeView()
        .tabItem { Label("Головна", systemImage: "house") }
        .tag(Tab.home)

    LibraryView()
        .tabItem { Label("Бібліотека", systemImage: "books.vertical") }
        .tag(Tab.library)
}

// ✅ Новий Tab API (iOS 18+, обов'язковий підхід для iOS 26)
enum AppTab: Hashable {
    case home, library, settings
}

@State private var selected: AppTab = .home

var body: some View {
    TabView(selection: $selected) {
        Tab("Головна", systemImage: "house", value: AppTab.home) {
            HomeView()
        }
        Tab("Бібліотека", systemImage: "books.vertical", value: AppTab.library) {
            LibraryView()
        }
        Tab("Налаштування", systemImage: "gearshape", value: AppTab.settings) {
            SettingsView()
        }
    }
}

Кілька моментів, які я хотів би, щоб мені хтось пояснив три роки тому:

  • Значення value: має бути Hashable. Enum, це найбільш чистий вибір, але можна використовувати UUID для динамічних вкладок.
  • Ярлик і піктограма, це не «декорація». Вони використовуються VoiceOver і в бічній панелі як текст рядка, тож не залишайте порожні Text("").
  • Якщо ви хочете кастомний вигляд ярлика, використовуйте перевантаження з замиканням: Tab(value: AppTab.home) { HomeView() } label: { Label { Text("Головна") } icon: { Image("home-custom") } }.
  • Для динамічного набору вкладок (наприклад, список плейлистів) TabView приймає ForEach прямо всередині: ForEach(playlists) { p in Tab(p.name, systemImage: "music.note", value: p.id) { PlaylistView(p) } }. Це поєднується з TabSection, як показано нижче.

Модифікатор .tabViewStyle(.sidebarAdaptable), це головна причина, чому Apple так активно перепаковує TabView. Він каже системі: «на компактному класі розміру я, вкладки внизу, на регулярному, бокова панель». Виглядає це так:

TabView(selection: $selected) {
    Tab("Головна", systemImage: "house", value: AppTab.home) { HomeView() }
    Tab("Бібліотека", systemImage: "books.vertical", value: AppTab.library) { LibraryView() }
    Tab("Профіль", systemImage: "person.crop.circle", value: AppTab.profile) { ProfileView() }
}
.tabViewStyle(.sidebarAdaptable)

Що відбувається на різних платформах:

  • iPhone (compact width). Стандартна нижня панель, максимум п’ять видимих вкладок, шоста і далі згортаються в «More».
  • iPad у portrait або split view. Може лишатися нижньою панеллю. Користувач може перемкнути на бокову за допомогою кнопки в кутку (з iOS 18 це стандартна поведінка).
  • iPad у landscape, повний екран. Бокова панель за замовчуванням. Виглядає як NavigationSplitView, але без вашого коду.
  • Mac Catalyst і macOS 26. Завжди бокова панель. Всі TabSection перетворюються на collapsible розділи.
  • visionOS 3. Бокова панель зліва як ornament, який слідує за вікном.

Коли не варто використовувати .sidebarAdaptable: якщо у вас лише 2–3 вкладки й вони не мають внутрішньої ієрархії, бокова панель на iPad виглядатиме порожньою. У такому разі краще залишити стандартний стиль. Так само, якщо ваш застосунок документоорієнтований (як Pages), розгляньте NavigationSplitView замість TabView. Тут я узгоджуюся з рекомендацією Human Interface Guidelines. Побічний ефект sidebarAdaptable, який мене колись збив з пантелику: коли користувач у split view звужує вікно, обрана вкладка зберігається, але scroll offset скидається. Це системна поведінка, не ваш баг.

Групування вкладок за допомогою TabSection

Уявіть застосунок для читання: у вас є «Home», «Library», «Search», а також список колекцій, які створив користувач. У бічній панелі на iPad такі колекції природно згрупувати. Раніше довелося б будувати паралельний NavigationSplitView; тепер це один TabSection:

TabView(selection: $selected) {
    Tab("Головна", systemImage: "house", value: AppTab.home) { HomeView() }
    Tab("Бібліотека", systemImage: "books.vertical", value: AppTab.library) { LibraryView() }

    TabSection("Мої колекції") {
        ForEach(store.collections) { collection in
            Tab(collection.name, systemImage: collection.iconName,
                value: AppTab.collection(collection.id)) {
                CollectionView(collection: collection)
            }
        }
    }

    TabSection("Спільні") {
        ForEach(store.sharedCollections) { collection in
            Tab(collection.name, systemImage: "person.2", value: AppTab.shared(collection.id)) {
                CollectionView(collection: collection)
            }
        }
    }

    Tab(role: .search, value: AppTab.search) { SearchView() }
}
.tabViewStyle(.sidebarAdaptable)

Що варто пам’ятати:

  • На iPhone TabSection згортається: усі вкладки з’являються поспіль у «More», без заголовків розділів. Це навмисно, панель знизу не має місця для groups.
  • На iPad і macOS заголовок TabSection, це клавіатурно доступний collapsible section header. Користувачі можуть згорнути секцію.
  • Значення value усередині ForEach має бути стабільним. Якщо ви передаватимете UUID() замість collection.id, при кожному ререндері SwiftUI вважатиме, що вкладка нова, і буде «мигання» стану (саме цей баг з’їв мені пів дня в одному з проєктів).
  • Обмежень на кількість секцій немає, але після 3–4 секцій навіть на iPad Pro стає незручно. Це UX-обмеження, а не технічне.

Як користувачі можуть налаштовувати вкладки

З iOS 18 TabView навчився нативної кастомізації: користувач заходить у режим редагування бічної панелі, перетягує вкладки і приховує ті, які йому не потрібні. Щоб це увімкнути, потрібен TabViewCustomization і .customizationID(...) для кожної вкладки, яку можна переміщувати:

@AppStorage("tabCustomization") private var customization = TabViewCustomization()

var body: some View {
    TabView(selection: $selected) {
        Tab("Головна", systemImage: "house", value: AppTab.home) { HomeView() }
            .customizationID("app.tab.home")

        TabSection("Мої колекції") {
            ForEach(store.collections) { collection in
                Tab(collection.name, systemImage: collection.iconName,
                    value: AppTab.collection(collection.id)) {
                    CollectionView(collection: collection)
                }
                .customizationID("app.tab.collection.\(collection.id)")
                .customizationBehavior(.reorderable, for: .sidebar)
            }
        }
        .customizationID("app.section.collections")
    }
    .tabViewStyle(.sidebarAdaptable)
    .tabViewCustomization($customization)
}

Про customizationBehavior(...): за замовчуванням вкладки з ForEach перетягувати не можна. Модифікатор .reorderable вмикає перетягування; .disabled закріплює вкладку на місці (корисно для «Головна», яку ви не хочете дозволяти ховати). Мій особистий досвід такий: завжди залишайте головну вкладку як .disabled для .sidebar і .tabBar, інакше користувач може приховати єдиний шлях назад і потім здивуватися, куди вона поділася.

Пошук у мобільних застосунках Apple майже завжди має власну «вкладку». Раніше ви створювали звичайну вкладку з іконкою лупи; тепер SwiftUI знає про це семантично:

Tab(role: .search, value: AppTab.search) {
    SearchView()
}

Що це дає:

  • На iPhone система сама розміщує вкладку в кінці панелі та застосовує стандартне оформлення (іконку лупи, локалізований ярлик «Пошук»).
  • На iPad і Mac у режимі бокової панелі пошук переїжджає у верхній toolbar, так само, як у Apple Music або Photos. Ви можете додати .searchable(text: $query) усередині SearchView, і SwiftUI поєднає їх.
  • Системний жест «swipe down з головного екрану iPad», який відкриває Spotlight, тепер може безпосередньо відкривати пошукову вкладку вашого застосунку, якщо ви задекларували її через role: .search.

Одна пастка: не намагайтеся передавати label у Tab(role: .search). Ярлик буде проігнорований, система завжди використовує локалізований «Search». Якщо вам потрібен кастомний ярлик, це знак, що семантично вкладка не є пошуковою й ви маєте використати звичайну Tab.

Кросплатформенна поведінка: iPad, Mac Catalyst і visionOS

Оскільки одна кодова база тепер обслуговує кілька форм-факторів, варто пройтися по нюансах кожної платформи. У власних проєктах я тестую всі три перед релізом, бо різниця в поведінці TabView помітна навіть на однаковому SwiftUI-дереві.

iPad

Регулярний клас розміру, бокова панель. Компактний (split view з 1/3, деякі iPad mini у portrait), нижня панель. Користувач може вручну перемкнути кнопкою біля бокової. Ваш код не міняється. Але: якщо ви покладаєтеся на UIDevice.current.userInterfaceIdiom, ви отримаєте .pad завжди, тоді як реальний UI буде інший. Використовуйте @Environment(\.horizontalSizeClass).

Mac Catalyst і macOS 26

TabView завжди бокова панель. Пункт меню «View → Show/Hide Sidebar» додається автоматично. Toolbar показує назву поточної вкладки, якщо ви задали .navigationTitle(...) у контенті. Кастомізація вкладок доступна через контекстне меню на бічній панелі.

visionOS 3

Бокова панель відображається як ornament, плаваюча смуга зліва від основного вікна. Іконки збільшуються, коли на них наведено погляд. Розділи TabSection розкриваються натисканням; вкладки з role: .search отримують окреме поле пошуку в ornament. Уникайте .tabViewStyle(.page) у visionOS, це не паджинація, а справжня навігація, і .page порушує очікування системи.

watchOS 12

TabView на watchOS має свою власну реальність: .sidebarAdaptable ігнорується, стилі за замовчуванням, це вертикально прокручувані «page» або «carousel». Я згадую це, бо документація Apple зараз перенаправляє з TabView на «Verticaly Paginated Views» на watchOS, і ви можете здивуватися, чому ваш код не компілюється однаково.

Deep linking і керування вибраною вкладкою

Оскільки TabView(selection: $selected) прив’язує вибрану вкладку до @State, глибокі посилання й widgets працюють просто: змінюєте значення, і SwiftUI перемикає вкладку. Тему маршрутизації NavigationStack та deep links у SwiftUI я описую окремо, а нижче тільки TabView-частина. Я хіба що згадаю, що для координації з віджетами та Live Activities в iOS 26 ця схема працює однаково.

@main
struct MyApp: App {
    @State private var selectedTab: AppTab = .home
    @State private var libraryPath = NavigationPath()

    var body: some Scene {
        WindowGroup {
            RootView(selectedTab: $selectedTab, libraryPath: $libraryPath)
                .onOpenURL { url in
                    handle(url: url)
                }
        }
    }

    private func handle(url: URL) {
        // /library/book/abc123
        let components = url.pathComponents.dropFirst()
        guard let first = components.first else { return }

        switch first {
        case "library":
            selectedTab = .library
            if components.count >= 3, components[1] == "book" {
                libraryPath.append(BookRoute.detail(id: components[2]))
            }
        case "search":
            selectedTab = .search
        default:
            break
        }
    }
}

Тут два важливі принципи. Перший: не тримайте selection у @StateObject-моделі, це створює зайвий шар і викликає стрибки анімації, коли ви програмно змінюєте вкладку. Використовуйте прямий @State у сцені й передавайте як @Binding. Другий: NavigationStack усередині кожної вкладки має власний NavigationPath. Якщо ви хочете глибоке посилання відкрити конкретний екран, спочатку перемкніть вкладку, а вже потім модифікуйте path, інакше NavigationStack ще не існує на екрані й append буде втрачено (це задокументована поведінка, а не баг).

Інтеграція з Liquid Glass у iOS 26

У iOS 26 нижня панель TabView і бокова панель отримали ефект Liquid Glass у SwiftUI. Це прозоре скло, яке заломлює контент за ним, а не просто розмиває. Практичні наслідки для розробника:

  • Приберіть свої фонові кольори з панелі. Якщо ви колись задавали UITabBar.appearance().backgroundColor = ... для того, щоб «зробити красиво», ви руйнуєте Liquid Glass. У iOS 26 система очікує, що ви покладете фон на контент, а не на панель.
  • Кольори іконок вкладок тепер автоматично адаптуються до контенту за панеллю. Не хардкодьте .foregroundStyle(.blue) для tabItem. Обрана вкладка вже виділяється системним акцентом, а не обраний, приглушеним варіантом того самого.
  • Тестуйте на світлому й темному фонах. Через прозорість те, що виглядало добре на білому background, може стати нечитабельним на темному контенті. У Xcode 26 у Previews є toggle «Under-content preview», який показує, як бокова панель виглядає над реальним контентом.
  • Ornament у visionOS завжди був прозорий; тепер поведінка узгоджена між платформами. Це полегшує адаптацію єдиного дизайну.

Якщо ваш проєкт використовує кастомний TabBar (наприклад, ви побудували власний нижче TabView з HStack + кнопок), настав час його викинути. У iOS 26 системний TabView робить майже все, що раніше було приводом для велосипеда: анімовані badges, кастомізацію, Liquid Glass, роль пошуку. Кастомний варіант доведеться підтримувати самостійно, а системний за вас підтримує Apple.

Офіційну документацію та деталі API дивіться в довіднику Apple з TabView, у довіднику типу Tab та в записі WWDC24 «Elevate your tab and sidebar experience in iPadOS». Саме там Apple вперше показала sidebarAdaptable і TabViewCustomization.

Поширені запитання

Чи можна змішувати старий tabItem і новий Tab у тому самому TabView?

Технічно компілятор пропустить, але це погана ідея. Кастомізація, TabSection і роль пошуку працюють лише зі старим API вкладок або лише з новим, а суміш ламає порядок і стан вибору. Мігруйте увесь TabView одразу.

Що відбувається на iPhone, коли ви використовуєте .sidebarAdaptable?

Нічого не змінюється: iPhone має лише компактний клас розміру, тож TabView залишається нижньою панеллю. Модифікатор безпечно ігнорується, не потрібно обгортати його в #if os(iOS).

Скільки вкладок можна відобразити?

На iPhone видно максимум п’ять, решта згортається у вкладку «Ще». На iPad і Mac у режимі бокової панелі обмежень немає, але понад 8–10 вкладок вимагає групування через TabSection, інакше користувач загубиться.

Як програмно перемкнути вкладку з іншого view?

Прокиньте @Binding селектора у дочірній view і присвойте нове значення, або використайте @Environment-обгортку з власним EnvironmentKey. Не використовуйте NotificationCenter: це працює, але порушує принцип single source of truth SwiftUI.

Чи підтримує TabView з sidebarAdaptable перетягування файлів у бокову панель?

Так, з iOS 26. Використовуйте .dropDestination(for: URL.self) усередині view вкладки, і система маршрутизує drop навіть коли користувач наводить на іконку вкладки в боковій панелі. Вкладка автоматично активується через 0.5 секунди.

Hiroshi Sato
Про Автора Hiroshi Sato

Apple Platforms specialist building for iOS, macOS, visionOS, and the occasional watchOS app nobody asked for.