Adaptive Layout — підхід до верстки інтерфейсів, при якому додаток використовує різні макети для різних типів пристроїв та орієнтацій екрана. На відміну від responsive design, adaptive layout перемикається між заздалегідь підготовленими макетами в точках breakpoint. Стаття розбирає Size Classes та UITraitCollection в iOS, кваліфікатори sw600dp/layout-w600dp в Android та наводить практичні приклади.
Головне
Adaptive Layout — стратегія побудови інтерфейсу, при якій розробник створює окремі макети для кожного типу пристроїв і перемикається між ними на основі характеристик екрана. На відміну від responsive design, де елементи плавно перетікають, adaptive layout використовує дискретні перемикання: телефон — один макет, планшет — інший, десктоп — третій.
Apple впровадила Size Classes у iOS 8 (2014) разом з універсальними додатками (universal binaries, що працюють і на iPhone, і на iPad). Google представила кваліфікатор smallestWidth (sw
Сучасні рекомендації Apple HIG та Google Material Design сходяться на тому, що adaptive layout обов'язковий для додатків, що підтримують більше одного розміру екрана. Google Play позначає відсутність адаптації під планшети як недолік. App Store не має формальної вимоги, але користувачі iPad очікують нативного досвіду, а не розтягнутого iPhone-додатка.
Size Classes — характеристика iOS, що визначає доступний простір як компактний (Compact) або регулярний (Regular) по кожній осі. UITraitCollection — система iOS для передачі характеристик середовища через ієрархію view: розмір, масштаб екрана, force touch capability, колірна гамма (світла/темна), accessibility settings.
| Пристрій / Орієнтація | Горизонталь | Вертикаль |
|---|---|---|
| iPhone портрет (всі моделі) | Compact | Regular |
| iPhone Plus/Pro Max ландшафт | Regular | Compact |
| iPhone SE / mini ландшафт | Compact | Compact |
| iPad портрет | Regular | Regular |
| iPad ландшафт | Regular | Regular |
| iPad Split View (1/3 екрана) | Compact | Regular |
UITraitCollection формується системою і передається від UIApplication через UIWindow до кожного UIView. При зміні орієнтації або розміру вікна (Split View) система генерує новий UITraitCollection і викликає traitCollectionDidChange. У SwiftUI зміни відстежуються через Environment Values і автоматично перемальовують view.
Адаптація через UITraitCollection в UIKit реалізується підпискою на зміни trait collection і перебудовою layout. Приклад нижче показує контролер, який перемикається між вертикальним стеком для iPhone і split-макетом для iPad.
// Адаптивний контролер з Size Classes
import UIKit
final class AdaptiveViewController: UIViewController {
private let compactStack = UIStackView()
private let regularStack = UIStackView()
private let sidebar = UIView()
private let content = UIView()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
updateLayout(for: traitCollection)
}
override func traitCollectionDidChange(
_ previousTraitCollection: UITraitCollection?
) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.horizontalSizeClass
!= previousTraitCollection?.horizontalSizeClass {
updateLayout(for: traitCollection)
}
}
private func updateLayout(for traits: UITraitCollection) {
if traits.horizontalSizeClass == .regular {
showRegularLayout() // iPad: sidebar + content
} else {
showCompactLayout() // iPhone: tab bar + push
}
}
private func showRegularLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
regularStack.addArrangedSubview(sidebar)
regularStack.addArrangedSubview(content)
regularStack.frame = view.bounds
regularStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(regularStack)
}
private func showCompactLayout() {
view.subviews.forEach { $0.removeFromSuperview() }
compactStack.addArrangedSubview(content)
compactStack.frame = view.bounds
compactStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(compactStack)
}
private func setupViews() {
sidebar.backgroundColor = .systemGray6
content.backgroundColor = .systemBackground
compactStack.axis = .vertical
regularStack.axis = .horizontal
}
}Метод traitCollectionDidChange викликається при будь-якій зміні traits, але ми перевіряємо тільки horizontalSizeClass — найбільш значущу ознаку для зміни макета. В iOS 17+ рекомендується використовувати UIViewController.horizontalSizeClass як computed property для реактивного оновлення без підписки на зміни.
Android надає два основних кваліфікатори ширини: sw
| Кваліфікатор | Умова | Приклад пристрою |
|---|---|---|
| layout-sw600dp | Мінімальна ширина ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Поточна ширина ≥ 600 dp | iPad у Split View (600 dp), телефон у ландшафті |
| layout-land | Орієнтація landscape | Будь-який пристрій повернуто горизонтально |
| layout-sw720dp | Мінімальна ширина ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Ієрархія ресурсів в Android: res/layout/activity_main.xml (телефон), res/layout-sw600dp/activity_main.xml (планшет 7"), res/layout-w600dp-land/activity_main.xml (телефон у ландшафті). Android вибирає найбільш специфічний кваліфікатор, що підходить під поточну конфігурацію пристрою. За відсутності відповідного файлу використовується базовий ресурс з res/layout/.
Сучасна Android-розробка використовує Jetpack WindowManager для визначення розміру вікна замість прямого читання ресурсів. Це особливо важливо для складних пристроїв, де розмір екрана може змінюватися динамічно (Galaxy Fold розкладається з 6.2" до 7.6").
// Адаптація через Jetpack WindowManager
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AdaptiveScreen() {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
FlowRow(
verticalAlignment = Arrangement.Top,
horizontalArrangement = when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.EXPANDED -> Arrangement.SpaceEvenly
else -> Arrangement.Start
}
) {
when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
CompactContent()
}
WindowWidthSizeClass.MEDIUM -> {
MediumContent()
}
WindowWidthSizeClass.EXPANDED -> {
ExpandedContent()
}
}
}
}
@Composable
private fun CompactContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text("Телефон", style = MaterialTheme.typography.headlineSmall)
ListContent()
}
}
@Composable
private fun MediumContent() {
Row(modifier = Modifier.padding(24.dp)) {
NavigationRail { ... }
Column { ListContent() }
}
}
@Composable
private fun ExpandedContent() {
Row(modifier = Modifier.padding(32.dp)) {
PermanentNavigationDrawer { ... }
Column {
ListContent()
}
DetailPane()
}
}Компонент currentWindowAdaptiveInfo() з бібліотеки androidx.window адаптує інтерфейс під будь-який пристрій: телефон, планшет, складний, ChromeOS. Класи ширини: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Класи висоти: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI надає вбудовані інструменти для адаптивного layout без прямого використання Size Classes. AnyLayout, ViewThatFits та GeometryReader дозволяють будувати інтерфейси, що автоматично підлаштовуються під доступний простір.
// SwiftUI адаптація через AnyLayout та ViewThatFits
import SwiftUI
struct AdaptiveGrid: View {
@Environment(\.horizontalSizeClass) private var hSizeClass
var body: some View {
let layout = hSizeClass == .regular
? AnyLayout(HStackLayout())
: AnyLayout(VStackLayout())
layout {
Label("Обране", systemImage: "star")
Label("Недавні", systemImage: "clock")
Label("Налаштування", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — автоматичний вибір layout
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Пріоритет 1: горизонтальний
CardView()
CardView()
}
VStack { // Якщо не влізло — вертикальний
CardView()
CardView()
}
}
}
}ViewThatFits — потужний інструмент SwiftUI, що автоматично вибирає перший дочірній view, який поміщається в доступний простір без обрізання. Це позбавляє від необхідності явно перевіряти Size Classes для простих адаптивних перемикань. AnyLayout дозволяє перемикати тип layout (HStack/VStack) без if-else в тілі view.
Jetpack Compose використовує WindowSizeClass (з бібліотеки Material 3 adaptive) та BoxWithConstraints для адаптивної верстки. На відміну від XML-кваліфікаторів, Compose визначає розмір вікна в runtime і перекомпоновує UI реактивно при зміні конфігурації.
// Jetpack Compose adaptive через BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Планшет: дві колонки (list-detail)
Row(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.weight(1f)) {
items(items) { item ->
ListItem(text = item)
}
}
var selected by remember { mutableStateOf(items.first()) }
DetailPanel(item = selected)
.weight(2f)
}
} else {
// Телефон: одна колонка з навігацією
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items) { item ->
ListItem(text = item, onClick = { navigateToDetail(item) })
}
}
}
}
}
@Composable
fun AdaptiveListMaterial3() {
val windowClass = currentWindowAdaptiveInfo().windowSizeClass
AdaptiveLayout(
layout = windowClass.windowWidthSizeClass
) {
when (windowClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
else -> {
ListDetailScaffold(
isDetailOnly = false,
list = { ListPane() },
detail = { DetailPane() }
)
}
}
}
}Material 3 Adaptive Layouts надають готові компоненти: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Ці компоненти автоматично адаптуються під WindowWidthSizeClass — перемикаються між stacked (COMPACT) та side-by-side (MEDIUM/EXPANDED). Розробнику достатньо вибрати патерн і передати контентні панелі.
Часті запитання
sw600dp (smallestWidth) — мінімальна ширина екрана незалежно від орієнтації. Спрацьовує на планшетах 7"+ завжди. w600dp (available width) — доступна ширина з урахуванням поточної орієнтації. Спрацьовує на телефонах у ландшафті, де ширина може перевищувати 600 dp.
UITraitCollection — об'єкт iOS, що містить характеристики середовища: size classes (horizontal/vertical), display scale, force touch capability, user interface idiom (iPhone/iPad), color gamut, accessibility settings. Передається від UIApplication через ієрархію view і змінюється при зміні орієнтації або розміру вікна.
Використовуйте Jetpack WindowManager (Android) та UIScreen nativeBounds + traitCollection (iOS). Складні пристрої змінюють Size Class при розкладанні. Рекомендується архітектура на основі канонічних layout (list-detail, supporting pane) з реактивною підпискою на зміни розміру вікна без перезапуску Activity.
Ні. Використовуйте один storyboard з Size Classes варіаціями constraint або програмний layout через UIKit. SwiftUI взагалі не потребує storyboards. Окремі storyboards для iPad створюють дублювання та ускладнюють підтримку — краще адаптувати один макет через Size Classes.
Canonical layouts — готові патерни адаптивної верстки від Google: list-detail (список + деталізація), supporting pane (контент + панель дій), feed (картки). Кожен патерн підтримує три стани (compact/medium/expanded) і реалізований у бібліотеці Material 3 Adaptive через ListDetailScaffold та SupportingPaneScaffold.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.