Adaptive Layout — подход к вёрстке интерфейсов, при котором приложение использует разные макеты для разных типов устройств и ориентаций экрана. В отличие от responsive design, adaptive layout переключается между заранее подготовленными лейаутами в точках breakpoint. Статья разбирает Size Classes и UITraitCollection в iOS, квалификаторы sw600dp/layout-w600dp в Android и приводит практические примеры.
Главное
Adaptive Layout — стратегия построения интерфейса, при которой разработчик создаёт отдельные макеты для каждого типа устройств и переключается между ними на основе характеристик экрана. В отличие от responsive design, где элементы плавно перетекают, adaptive layout использует дискретные переключения: телефон — один 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, цветовая гамма (light/dark), accessibility settings.
| Устройство / Ориентация | Horizontal | Vertical |
|---|---|---|
| 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также