Adaptive Layout — een benadering van interface-ontwerp waarbij de app verschillende lay-outs gebruikt voor verschillende typen apparaten en schermoriëntaties. In tegenstelling tot responsive design schakelt adaptive layout tussen vooraf voorbereide lay-outs op breakpoint-punten. Het artikel behandelt Size Classes en UITraitCollection in iOS, kwalificatoren sw600dp/layout-w600dp in Android en geeft praktische voorbeelden.
Belangrijkste punten
Adaptive Layout — een strategie voor het bouwen van interfaces waarbij de ontwikkelaar aparte lay-outs maakt voor elk apparaattype en schakelt tussen deze op basis van schermkenmerken. In tegenstelling tot responsive design, waar elementen vloeiend stromen, gebruikt adaptive layout discrete schakelingen: telefoon — één lay-out, tablet — een andere, desktop — een derde.
Apple introduceerde Size Classes in iOS 8 (2014) samen met universele apps (universal binaries die zowel op iPhone als iPad werken). Google introduceerde de smallestWidth (sw
Moderne aanbevelingen van Apple HIG en Google Material Design zijn het erover eens dat adaptive layout verplicht is voor apps die meer dan één schermgrootte ondersteunen. Google Play markeert het ontbreken van aanpassing voor tablets als een tekortkoming. De App Store heeft geen formele vereiste, maar iPad-gebruikers verwachten een native ervaring, niet een uitgerekte iPhone-app.
Size Classes — iOS-kenmerk dat de beschikbare ruimte definieert als Compact of Regular op elke as. UITraitCollection — het iOS-systeem voor het doorgeven van omgevingskenmerken door de view-hiërarchie: grootte, schermschaal, force touch capability, kleurengamma (light/dark), accessibility settings.
| Apparaat / Oriëntatie | Horizontal | Vertical |
|---|---|---|
| iPhone portret (alle modellen) | Compact | Regular |
| iPhone Plus/Pro Max landschap | Regular | Compact |
| iPhone SE / mini landschap | Compact | Compact |
| iPad portret | Regular | Regular |
| iPad landschap | Regular | Regular |
| iPad Split View (1/3 scherm) | Compact | Regular |
UITraitCollection wordt door het systeem gevormd en doorgegeven van UIApplication via UIWindow naar elke UIView. Bij wijziging van oriëntatie of venstergrootte (Split View) genereert het systeem een nieuwe UITraitCollection en roept traitCollectionDidChange aan. In SwiftUI worden wijzigingen gevolgd via Environment Values en wordt de view automatisch opnieuw getekend.
Aanpassing via UITraitCollection in UIKit wordt geïmplementeerd door te abonneren op trait collection wijzigingen en de lay-out te herbouwen. Het onderstaande voorbeeld toont een controller die schakelt tussen een verticale stack voor iPhone en een split-lay-out voor iPad.
// Adaptieve controller met 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: zijbalk + inhoud
} else {
showCompactLayout() // iPhone: tabbalk + 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
}
}De methode traitCollectionDidChange wordt aangeroepen bij elke wijziging van traits, maar we controleren alleen horizontalSizeClass — de belangrijkste indicator voor lay-out wijziging. In iOS 17+ wordt aanbevolen om UIViewController.horizontalSizeClass te gebruiken als computed property voor reactieve updates zonder te abonneren op wijzigingen.
Android biedt twee belangrijke breedte-kwalificatoren: sw
| Kwalificator | Voorwaarde | Voorbeeld apparaat |
|---|---|---|
| layout-sw600dp | Minimale breedte ≥ 600 dp | iPad (768 dp), Pixel C (900 dp) |
| layout-w600dp | Huidige breedte ≥ 600 dp | iPad in Split View (600 dp), telefoon in landschap |
| layout-land | Landschapsoriëntatie | Elk apparaat horizontaal gedraaid |
| layout-sw720dp | Minimale breedte ≥ 720 dp | iPad Pro (833 dp), Galaxy Tab S9 (800 dp) |
Resource hiërarchie in Android: res/layout/activity_main.xml (telefoon), res/layout-sw600dp/activity_main.xml (tablet 7"), res/layout-w600dp-land/activity_main.xml (telefoon in landschap). Android kiest de meest specifieke kwalificator die past bij de huidige apparaatconfiguratie. Bij afwezigheid van een geschikt bestand wordt de basisresource uit res/layout/ gebruikt.
Moderne Android-ontwikkeling gebruikt Jetpack WindowManager om de venstergrootte te bepalen in plaats van directe uitlezing van resources. Dit is vooral belangrijk voor opvouwbare apparaten, waar de schermgrootte dynamisch kan veranderen (Galaxy Fold vouwt uit van 6,2" naar 7,6").
// Aanpassing 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("Telefoon", 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()
}
}Het component currentWindowAdaptiveInfo() uit de androidx.window bibliotheek past de interface aan elk apparaat aan: telefoon, tablet, opvouwbaar, ChromeOS. Breedteklassen: COMPACT (0–600 dp), MEDIUM (600–840 dp), EXPANDED (>840 dp). Hoogteklassen: COMPACT (0–480 dp), MEDIUM (480–900 dp), EXPANDED (>900 dp).
SwiftUI biedt ingebouwde hulpmiddelen voor adaptieve lay-out zonder direct gebruik van Size Classes. AnyLayout, ViewThatFits en GeometryReader maken het mogelijk om interfaces te bouwen die zich automatisch aanpassen aan de beschikbare ruimte.
// SwiftUI-aanpassing via AnyLayout en 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("Favorieten", systemImage: "star")
Label("Recent", systemImage: "clock")
Label("Instellingen", systemImage: "gear")
}
.padding()
}
}
// ViewThatFits — automatische lay-outselectie
struct SmartLayout: View {
var body: some View {
ViewThatFits {
HStack { // Prioriteit 1: horizontaal
CardView()
CardView()
}
VStack { // Als het niet past — verticaal
CardView()
CardView()
}
}
}
}ViewThatFits — een krachtig SwiftUI-hulpmiddel dat automatisch de eerste child-view selecteert die in de beschikbare ruimte past zonder te worden afgesneden. Dit elimineert de noodzaak om expliciet Size Classes te controleren voor eenvoudige adaptieve schakelingen. AnyLayout maakt het mogelijk om het lay-outtype (HStack/VStack) te wijzigen zonder if-else in de body van de view.
Jetpack Compose gebruikt WindowSizeClass (uit de Material 3 adaptive bibliotheek) en BoxWithConstraints voor adaptieve lay-out. In tegenstelling tot XML-kwalificatoren bepaalt Compose de venstergrootte tijdens runtime en herbouwt de UI reactief bij configuratiewijzigingen.
// Jetpack Compose-aanpassing via BoxWithConstraints
@Composable
fun AdaptiveList(items: List<String>) {
BoxWithConstraints {
val width = maxWidth
if (width >= 600.dp) {
// Tablet: twee kolommen (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 {
// Telefoon: één kolom met navigatie
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 biedt kant-en-klare componenten: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. Deze componenten passen zich automatisch aan WindowWidthSizeClass aan — schakelen tussen stacked (COMPACT) en side-by-side (MEDIUM/EXPANDED). De ontwikkelaar hoeft alleen het patroon te selecteren en de inhoudspanelen door te geven.
Veelgestelde vragen
sw600dp (smallestWidth) — minimale schermbreedte ongeacht oriëntatie. Werkt altijd op tablets van 7"+. w600dp (available width) — beschikbare breedte rekening houdend met de huidige oriëntatie. Werkt op telefoons in landschap, waar de breedte 600 dp kan overschrijden.
UITraitCollection — een iOS-object met omgevingskenmerken: size classes (horizontal/vertical), display scale, force touch capability, user interface idiom (iPhone/iPad), color gamut, accessibility settings. Het wordt doorgegeven van UIApplication via de view-hiërarchie en verandert bij wijziging van oriëntatie of venstergrootte.
Gebruik Jetpack WindowManager (Android) en UIScreen nativeBounds + traitCollection (iOS). Opvouwbare apparaten veranderen de Size Class bij het uitvouwen. Een architectuur op basis van canonieke lay-outs (list-detail, supporting pane) met reactieve abonnering op venstergroottewijzigingen zonder Activity-herstart wordt aanbevolen.
Nee. Gebruik één storyboard met Size Classes constraint-variaties of programmatische lay-out via UIKit. SwiftUI vereist helemaal geen storyboards. Aparte storyboards voor iPad creëren duplicatie en compliceren onderhoud — het is beter om één lay-out aan te passen via Size Classes.
Canonical layouts — kant-en-klare adaptieve lay-outpatronen van Google: list-detail (lijst + details), supporting pane (inhoud + actiepaneel), feed (kaarten). Elk patroon ondersteunt drie toestanden (compact/medium/expanded) en is geïmplementeerd in de Material 3 Adaptive bibliotheek via ListDetailScaffold en SupportingPaneScaffold.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook