TabView у SwiftUI для iOS 26: Tab API, sidebarAdaptable і адаптивна навігація
Практичний посібник з нового TabView у SwiftUI для iOS 26: новий тип Tab, стиль sidebarAdaptable, TabSection, роль пошуку, кастомізація й адаптація до iPad, Mac Catalyst і visionOS з прикладами коду.
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, як показано нижче.
Стиль sidebarAdaptable: коли адаптувати навігацію
Модифікатор .tabViewStyle(.sidebarAdaptable), це головна причина, чому Apple так активно перепаковує TabView. Він каже системі: «на компактному класі розміру я, вкладки внизу, на регулярному, бокова панель». Виглядає це так:
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:
На iPhoneTabSection згортається: усі вкладки з’являються поспіль у «More», без заголовків розділів. Це навмисно, панель знизу не має місця для groups.
На iPad і macOS заголовок TabSection, це клавіатурно доступний collapsible section header. Користувачі можуть згорнути секцію.
Значення value усередині ForEach має бути стабільним. Якщо ви передаватимете UUID() замість collection.id, при кожному ререндері SwiftUI вважатиме, що вкладка нова, і буде «мигання» стану (саме цей баг з’їв мені пів дня в одному з проєктів).
Обмежень на кількість секцій немає, але після 3–4 секцій навіть на iPad Pro стає незручно. Це UX-обмеження, а не технічне.
Як користувачі можуть налаштовувати вкладки
З iOS 18 TabView навчився нативної кастомізації: користувач заходить у режим редагування бічної панелі, перетягує вкладки і приховує ті, які йому не потрібні. Щоб це увімкнути, потрібен TabViewCustomization і .customizationID(...) для кожної вкладки, яку можна переміщувати:
Про customizationBehavior(...): за замовчуванням вкладки з ForEach перетягувати не можна. Модифікатор .reorderable вмикає перетягування; .disabled закріплює вкладку на місці (корисно для «Головна», яку ви не хочете дозволяти ховати). Мій особистий досвід такий: завжди залишайте головну вкладку як .disabled для .sidebar і .tabBar, інакше користувач може приховати єдиний шлях назад і потім здивуватися, куди вона поділася.
TabRole.search: правильне місце для пошуку
Пошук у мобільних застосунках Apple майже завжди має власну «вкладку». Раніше ви створювали звичайну вкладку з іконкою лупи; тепер SwiftUI знає про це семантично:
На 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.
Чи можна змішувати старий 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 секунди.
Розберемось, як макрос @Observable у Swift замінює ObservableObject у SwiftUI: гранулярне стеження, @Bindable, @ObservationIgnored, withObservationTracking та новинки iOS 26.
Практичний посібник з хаптики у SwiftUI для iOS 26: модифікатор .sensoryFeedback з 11 типами, умовні тригери, Core Haptics та власні AHAP-патерни з реальними значеннями intensity і sharpness.
NavigationStack у SwiftUI для iOS 26: типобезпечна маршрутизація через navigationDestination, глибокі посилання з onOpenURL, збереження шляху через SceneStorage і робочі приклади коду Swift 6.2.