Adaptive Layout is an approach to interface layout where the application uses different layouts for different device types and screen orientations. Unlike responsive design, adaptive layout switches between pre-built layouts at breakpoints. This article covers Size Classes and UITraitCollection in iOS, sw600dp/layout-w600dp qualifiers in Android, and provides practical examples.
Key Takeaways
Adaptive Layout is a UI strategy where the developer creates separate layouts for each device type and switches between them based on screen characteristics. Unlike responsive design where elements flow smoothly, adaptive layout uses discrete switching: phone — one layout, tablet — another, desktop — a third.
Apple introduced Size Classes in iOS 8 (2014) alongside universal binaries (apps running on both iPhone and iPad). Google introduced the smallestWidth qualifier (sw
Modern Apple HIG and Google Material Design guidelines agree that adaptive layout is essential for apps supporting more than one screen size. Google Play marks the lack of tablet adaptation as a drawback. The App Store has no formal requirement, but iPad users expect a native experience, not a stretched iPhone app.
Size Classes are an iOS characteristic that defines available space as Compact or Regular on each axis. UITraitCollection is an iOS system for passing environment characteristics through the view hierarchy: size, screen scale, force touch capability, color gamut (light/dark), accessibility settings.
| Device / Orientation | Horizontal | Vertical |
|---|---|---|
| iPhone portrait (all models) | Compact | Regular |
| iPhone Plus/Pro Max landscape | Regular | Compact |
| iPhone SE / mini landscape | Compact | Compact |
| iPad portrait | Regular | Regular |
| iPad landscape | Regular | Regular |
| iPad Split View (1/3 screen) | Compact | Regular |
UITraitCollection is formed by the system and passed from UIApplication through UIWindow to each UIView. When orientation or window size changes (Split View), the system generates a new UITraitCollection and calls traitCollectionDidChange. In SwiftUI, changes are tracked through Environment Values and automatically redraw the view.
Adaptation via UITraitCollection in UIKit is implemented by subscribing to trait collection changes and rebuilding the layout. The example below shows a controller that switches between a vertical stack for iPhone and a split layout for iPad.
// Adaptive Controller with 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
}
}The traitCollectionDidChange method is called on any trait change, but we only check horizontalSizeClass — the most significant indicator for layout switching. In iOS 17+ it is recommended to use UIViewController.horizontalSizeClass as a computed property for reactive updates without subscribing to changes.
Android provides two main width qualifiers: sw
| Qualifier | Condition | Example Device |
|---|---|---|
| layout-sw600dp | Minimum width ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Current width ≥ 600 dp | iPad in Split View (600 dp), phone in landscape |
| layout-land | Landscape orientation | Any device rotated horizontally |
| layout-sw720dp | Minimum width ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Android resource hierarchy: res/layout/activity_main.xml (phone), res/layout-sw600dp/activity_main.xml (7" tablet), res/layout-w600dp-land/activity_main.xml (phone in landscape). Android selects the most specific qualifier matching the current device configuration. When no matching file exists, the base resource from res/layout/ is used.
Modern Android development uses Jetpack WindowManager to determine window size instead of directly reading resources. This is especially important for foldable devices where screen size changes dynamically (Galaxy Fold unfolds from 6.2" to 7.6").
// Adaptation via 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("Phone", 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()
}
}The currentWindowAdaptiveInfo() component from the androidx.window library adapts the interface for any device: phone, tablet, foldable, ChromeOS. Width classes: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Height classes: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI provides built-in tools for adaptive layout without directly using Size Classes. AnyLayout, ViewThatFits, and GeometryReader allow building interfaces that automatically adjust to available space.
// SwiftUI adaptation via AnyLayout and 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("Favorites", systemImage: "star")
Label("Recent", systemImage: "clock")
Label("Settings", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — automatic layout selection
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Priority 1: horizontal
CardView()
CardView()
}
VStack { // If it doesn't fit — vertical
CardView()
CardView()
}
}
}
}ViewThatFits is a powerful SwiftUI tool that automatically selects the first child view that fits in the available space without clipping. This eliminates the need to explicitly check Size Classes for simple adaptive switches. AnyLayout allows switching layout type (HStack/VStack) without if-else in the view body.
Jetpack Compose uses WindowSizeClass (from the Material 3 adaptive library) and BoxWithConstraints for adaptive layout. Unlike XML qualifiers, Compose determines window size at runtime and reactively recomposes the UI on configuration changes.
// Jetpack Compose adaptive via BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Tablet: two columns (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 {
// Phone: one column with navigation
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 provide ready-made components: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. These components automatically adapt to WindowWidthSizeClass — switching between stacked (COMPACT) and side-by-side (MEDIUM/EXPANDED). The developer just needs to choose a pattern and pass content panels.
Frequently Asked Questions
sw600dp (smallestWidth) — minimum screen width regardless of orientation. Activates on 7"+ tablets always. w600dp (available width) — available width considering the current orientation. Activates on phones in landscape where width may exceed 600 dp.
UITraitCollection is an iOS object containing environment characteristics: size classes (horizontal/vertical), display scale, force touch capability, user interface idiom (iPhone/iPad), color gamut, accessibility settings. It is passed from UIApplication through the view hierarchy and changes when orientation or window size changes.
Use Jetpack WindowManager (Android) and UIScreen nativeBounds + traitCollection (iOS). Foldable devices change Size Class when unfolding. An architecture based on canonical layouts (list-detail, supporting pane) with reactive subscription to window size changes without Activity restart is recommended.
No. Use a single storyboard with Size Class constraint variations or programmatic layout via UIKit. SwiftUI does not require storyboards at all. Separate storyboards for iPad create duplication and complicate maintenance — it is better to adapt one layout through Size Classes.
Canonical layouts are ready-made adaptive layout patterns from Google: list-detail (list + detail), supporting pane (content + action panel), feed (cards). Each pattern supports three states (compact/medium/expanded) and is implemented in the Material 3 Adaptive library through ListDetailScaffold and SupportingPaneScaffold.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also