Adaptive Layout — podejście do projektowania interfejsów, w którym aplikacja używa różnych układów dla różnych typów urządzeń i orientacji ekranu. W przeciwieństwie do responsive design, adaptive layout przełącza się między wcześniej przygotowanymi układami w punktach breakpoint. Artykuł omawia Size Classes i UITraitCollection w iOS, kwalifikatory sw600dp/layout-w600dp w Android oraz podaje praktyczne przykłady.
Najważniejsze
Adaptive Layout — strategia budowania interfejsu, w której programista tworzy osobne układy dla każdego typu urządzenia i przełącza się między nimi na podstawie cech ekranu. W przeciwieństwie do responsive design, gdzie elementy płynnie się przemieszczają, adaptive layout używa dyskretnych przełączeń: telefon — jeden layout, tablet — drugi, desktop — trzeci.
Apple wprowadziło Size Classes w iOS 8 (2014) wraz z uniwersalnymi aplikacjami (universal binaries, działającymi zarówno na iPhone, jak i iPad). Google przedstawiła kwalifikator smallestWidth (sw
Nowoczesne zalecenia Apple HIG i Google Material Design są zgodne, że adaptive layout jest obowiązkowy dla aplikacji obsługujących więcej niż jeden rozmiar ekranu. Google Play oznacza brak adaptacji dla tabletów jako wadę. App Store nie ma formalnego wymogu, ale użytkownicy iPada oczekują natywnego doświadczenia, a nie rozciągniętej aplikacji iPhone'a.
Size Classes — cecha iOS określająca dostępną przestrzeń jako kompaktową (Compact) lub regularną (Regular) w każdej osi. UITraitCollection — system iOS do przekazywania cech środowiska przez hierarchię view: rozmiar, skalę ekranu, force touch capability, gamę kolorów (light/dark), accessibility settings.
| Urządzenie / Orientacja | Horizontal | Vertical |
|---|---|---|
| iPhone portret (wszystkie modele) | Compact | Regular |
| iPhone Plus/Pro Max krajobraz | Regular | Compact |
| iPhone SE / mini krajobraz | Compact | Compact |
| iPad portret | Regular | Regular |
| iPad krajobraz | Regular | Regular |
| iPad Split View (1/3 ekranu) | Compact | Regular |
UITraitCollection jest tworzony przez system i przekazywany od UIApplication przez UIWindow do każdego UIView. Przy zmianie orientacji lub rozmiaru okna (Split View) system generuje nowy UITraitCollection i wywołuje traitCollectionDidChange. W SwiftUI zmiany są śledzone przez Environment Values i automatycznie przerysowują view.
Adaptacja przez UITraitCollection w UIKit jest realizowana przez subskrypcję zmian trait collection i przebudowę layoutu. Poniższy przykład pokazuje kontroler, który przełącza się między pionowym stackiem dla iPhone'a a split-układem dla iPada.
// Adaptacyjny kontroler z 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: pasek kart + 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
}
}Metoda traitCollectionDidChange jest wywoływana przy każdej zmianie traits, ale sprawdzamy tylko horizontalSizeClass — najważniejszy wskaźnik do zmiany układu. W iOS 17+ zaleca się używanie UIViewController.horizontalSizeClass jako computed property do reaktywnego odświeżania bez subskrypcji zmian.
Android udostępnia dwa główne kwalifikatory szerokości: sw
| Kwalifikator | Warunek | Przykład urządzenia |
|---|---|---|
| layout-sw600dp | Minimalna szerokość ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Bieżąca szerokość ≥ 600 dp | iPad w Split View (600 dp), telefon w krajobrazie |
| layout-land | Orientacja krajobraz | Dowolne urządzenie obrócone poziomo |
| layout-sw720dp | Minimalna szerokość ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Hierarchia zasobów w Android: res/layout/activity_main.xml (telefon), res/layout-sw600dp/activity_main.xml (tablet 7"), res/layout-w600dp-land/activity_main.xml (telefon w krajobrazie). Android wybiera najbardziej specyficzny kwalifikator pasujący do bieżącej konfiguracji urządzenia. W przypadku braku odpowiedniego pliku używany jest podstawowy zasób z res/layout/.
Nowoczesne programowanie na Androida używa Jetpack WindowManager do określania rozmiaru okna zamiast bezpośredniego odczytu zasobów. Jest to szczególnie ważne dla urządzeń składanych, gdzie rozmiar ekranu może zmieniać się dynamicznie (Galaxy Fold rozwija się z 6,2" do 7,6").
// Adaptacja przez 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("Telefon", 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()
}
}Komponent currentWindowAdaptiveInfo() z biblioteki androidx.window adaptuje interfejs do dowolnego urządzenia: telefonu, tabletu, składanego, ChromeOS. Klasy szerokości: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Klasy wysokości: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI udostępnia wbudowane narzędzia do adaptacyjnego layoutu bez bezpośredniego używania Size Classes. AnyLayout, ViewThatFits i GeometryReader pozwalają budować interfejsy automatycznie dostosowujące się do dostępnej przestrzeni.
// Adaptacja SwiftUI przez AnyLayout i 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("Ulubione", systemImage: "star")
Label("Ostatnie", systemImage: "clock")
Label("Ustawienia", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — automatyczny wybór layoutu
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Priorytet 1: poziomy
CardView()
CardView()
}
VStack { // Jeśli się nie mieści — pionowy
CardView()
CardView()
}
}
}
}ViewThatFits — potężne narzędzie SwiftUI, automatycznie wybierające pierwszy podrzędny view, który mieści się w dostępnej przestrzeni bez przycinania. Eliminuje to potrzebę jawnego sprawdzania Size Classes dla prostych adaptacyjnych przełączeń. AnyLayout pozwala przełączać typ layoutu (HStack/VStack) bez if-else w ciele view.
Jetpack Compose używa WindowSizeClass (z biblioteki Material 3 adaptive) i BoxWithConstraints do adaptacyjnego układu. W przeciwieństwie do kwalifikatorów XML, Compose określa rozmiar okna w runtime i reaktywnie przebudowuje UI przy zmianie konfiguracji.
// Adaptacyjny Jetpack Compose przez BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Tablet: dwie kolumny (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 {
// Telefon: jedna kolumna z nawigacją
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 udostępnia gotowe komponenty: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Komponenty te automatycznie dostosowują się do WindowWidthSizeClass — przełączają się między stacked (COMPACT) a side-by-side (MEDIUM/EXPANDED). Programista musi tylko wybrać wzorzec i przekazać panele treści.
Często zadawane pytania
sw600dp (smallestWidth) — minimalna szerokość ekranu niezależnie od orientacji. Działa na tabletach 7"+ zawsze. w600dp (available width) — dostępna szerokość uwzględniająca bieżącą orientację. Działa na telefonach w krajobrazie, gdzie szerokość może przekraczać 600 dp.
UITraitCollection — obiekt iOS zawierający cechy środowiska: size classes (horizontal/vertical), display scale, force touch capability, user interface idiom (iPhone/iPad), color gamut, accessibility settings. Jest przekazywany od UIApplication przez hierarchię view i zmienia się przy zmianie orientacji lub rozmiaru okna.
Używaj Jetpack WindowManager (Android) i UIScreen nativeBounds + traitCollection (iOS). Urządzenia składane zmieniają Size Class przy rozkładaniu. Zalecana jest architektura oparta na kanonicznych layoutach (list-detail, supporting pane) z reaktywną subskrypcją zmian rozmiaru okna bez restartu Activity.
Nie. Używaj jednego storyboard z wariacjami Size Classes constraint lub programowego layoutu przez UIKit. SwiftUI w ogóle nie wymaga storyboards. Osobne storyboards dla iPada tworzą powielanie i utrudniają utrzymanie — lepiej dostosować jeden układ przez Size Classes.
Canonical layouts — gotowe wzorce adaptacyjnego układu od Google: list-detail (lista + szczegóły), supporting pane (treść + panel działań), feed (karty). Każdy wzorzec obsługuje trzy stany (compact/medium/expanded) i jest zaimplementowany w bibliotece Material 3 Adaptive przez ListDetailScaffold i SupportingPaneScaffold.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również