Оновлено: 29 липня 2026 р.
TabView у SwiftUI для iOS 26 — це компонент навігації верхнього рівня, який показує окремі розділи застосунку через вкладки внизу на iPhone і автоматично перетворюється на бокову панель на iPad, Mac Catalyst та visionOS, коли ви застосовуєте стиль .sidebarAdaptable. Замість застарілого ініціалізатора з ярликом і .tag(...) Apple рекомендує новий тип Tab, який підтримує групування через TabSection, кастомізацію користувачем, TabRole.search і візуальну інтеграцію з 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. Він каже системі: «на компактному класі розміру я, вкладки внизу, на регулярному, бокова панель». Виглядає це так:
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, інакше користувач може приховати єдиний шлях назад і потім здивуватися, куди вона поділася.
TabRole.search: правильне місце для пошуку
Пошук у мобільних застосунках 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 секунди.




